From 421b33ba9f678a9acd0195c8672730ceaf5f0bb7 Mon Sep 17 00:00:00 2001 From: taoerman Date: Sun, 23 Nov 2025 09:31:04 -0800 Subject: [PATCH 01/16] Community Library Notifications Filtering --- .../migrations/0158_auto_20251122_2351.py | 35 ++ contentcuration/contentcuration/models.py | 5 +- .../test_community_library_submission.py | 324 +++++++++++++++++- .../viewsets/community_library_submission.py | 61 ++-- 4 files changed, 399 insertions(+), 26 deletions(-) create mode 100644 contentcuration/contentcuration/migrations/0158_auto_20251122_2351.py diff --git a/contentcuration/contentcuration/migrations/0158_auto_20251122_2351.py b/contentcuration/contentcuration/migrations/0158_auto_20251122_2351.py new file mode 100644 index 0000000000..3d12278325 --- /dev/null +++ b/contentcuration/contentcuration/migrations/0158_auto_20251122_2351.py @@ -0,0 +1,35 @@ +# Generated by Django 3.2.24 on 2025-11-22 23:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contentcuration', '0157_merge_20251015_0333'), + ] + + operations = [ + migrations.RemoveIndex( + model_name='communitylibrarysubmission', + name='submission_date_created_idx', + ), + migrations.RemoveField( + model_name='communitylibrarysubmission', + name='date_resolved', + ), + migrations.AddField( + model_name='communitylibrarysubmission', + name='date_updated', + field=models.DateTimeField(blank=True, db_index=True, null=True), + ), + migrations.AlterField( + model_name='communitylibrarysubmission', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected'), ('SUPERSEDED', 'Superseded'), ('LIVE', 'Live')], db_index=True, default='PENDING', max_length=20), + ), + migrations.AddIndex( + model_name='communitylibrarysubmission', + index=models.Index(fields=['-date_updated'], name='submission_date_updated_idx'), + ), + ] diff --git a/contentcuration/contentcuration/models.py b/contentcuration/contentcuration/models.py index 84ecb2690f..e72b9d5fe2 100644 --- a/contentcuration/contentcuration/models.py +++ b/contentcuration/contentcuration/models.py @@ -2594,11 +2594,12 @@ class CommunityLibrarySubmission(models.Model): ) categories = models.JSONField(blank=True, null=True) date_created = models.DateTimeField(auto_now_add=True) - date_resolved = models.DateTimeField(blank=True, null=True) + date_updated = models.DateTimeField(blank=True, null=True, db_index=True) status = models.CharField( max_length=20, choices=community_library_submission.status_choices, default=community_library_submission.STATUS_PENDING, + db_index=True, ) resolved_by = models.ForeignKey( User, @@ -2704,7 +2705,7 @@ class Meta: ] indexes = [ # Useful for cursor pagination - models.Index(fields=["-date_created"], name="submission_date_created_idx"), + models.Index(fields=["-date_updated"], name="submission_date_updated_idx"), ] diff --git a/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py b/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py index acb73dd6e5..02b14ca01a 100644 --- a/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py +++ b/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py @@ -564,7 +564,7 @@ def _manually_reject_submission(self): ) self.submission.feedback_notes = self.feedback_notes self.submission.internal_notes = self.internal_notes - self.submission.date_resolved = self.resolved_time + self.submission.date_updated = self.resolved_time self.submission.save() def _refresh_submissions_from_db(self): @@ -721,7 +721,7 @@ def test_resolve_submission__accept_correct(self, apply_task_mock): self.assertEqual(resolved_submission.feedback_notes, self.feedback_notes) self.assertEqual(resolved_submission.internal_notes, self.internal_notes) self.assertEqual(resolved_submission.resolved_by, self.admin_user) - self.assertEqual(resolved_submission.date_resolved, self.resolved_time) + self.assertEqual(resolved_submission.date_updated, self.resolved_time) self.assertTrue( Change.objects.filter( @@ -763,7 +763,7 @@ def test_resolve_submission__reject_correct(self, apply_task_mock): self.assertEqual(resolved_submission.feedback_notes, self.feedback_notes) self.assertEqual(resolved_submission.internal_notes, self.internal_notes) self.assertEqual(resolved_submission.resolved_by, self.admin_user) - self.assertEqual(resolved_submission.date_resolved, self.resolved_time) + self.assertEqual(resolved_submission.date_updated, self.resolved_time) self.assertFalse( Change.objects.filter( @@ -905,3 +905,321 @@ def test_resolve_submission__reject_do_not_mark_superseded(self): self.submission_for_other_channel.status, community_library_submission_constants.STATUS_PENDING, ) + + +class FilteringAndSearchTestCase(StudioAPITestCase): + """ + Test cases for the new filtering and search functionality added for the notifications page. + Tests date_updated filters, status__in filter, and channel name search. + """ + + def setUp(self): + super().setUp() + self.editor_user = testdata.user(email="editor@user.com") + self.admin_user = testdata.user(email="admin@user.com") + self.admin_user.is_admin = True + self.admin_user.first_name = "Admin" + self.admin_user.last_name = "User" + self.admin_user.save() + + self.ensure_db_exists_patcher = mock.patch( + "contentcuration.utils.publish.ensure_versioned_database_exists" + ) + self.ensure_db_exists_patcher.start() + + self.math_channel = testdata.channel(name="Math Basics") + self.math_channel.public = False + self.math_channel.version = 1 + self.math_channel.editors.add(self.editor_user) + self.math_channel.save() + + self.science_channel = testdata.channel(name="Science Fundamentals") + self.science_channel.public = False + self.science_channel.version = 1 + self.science_channel.editors.add(self.editor_user) + self.science_channel.save() + + self.history_channel = testdata.channel(name="World History") + self.history_channel.public = False + self.history_channel.version = 1 + self.history_channel.editors.add(self.editor_user) + self.history_channel.save() + + self.math_advanced_channel = testdata.channel(name="Advanced Math") + self.math_advanced_channel.public = False + self.math_advanced_channel.version = 1 + self.math_advanced_channel.editors.add(self.editor_user) + self.math_advanced_channel.save() + + self.old_pending_submission = CommunityLibrarySubmission.objects.create( + channel=self.math_channel, + author=self.editor_user, + channel_version=1, + status=community_library_submission_constants.STATUS_PENDING, + date_created=datetime.datetime(2023, 1, 1, tzinfo=pytz.utc), + ) + self.old_pending_submission.date_updated = datetime.datetime( + 2023, 1, 1, tzinfo=pytz.utc + ) + self.old_pending_submission.save() + + self.recent_approved_submission = CommunityLibrarySubmission.objects.create( + channel=self.science_channel, + author=self.editor_user, + channel_version=1, + status=community_library_submission_constants.STATUS_APPROVED, + date_created=datetime.datetime(2024, 6, 1, tzinfo=pytz.utc), + resolved_by=self.admin_user, + ) + self.recent_approved_submission.date_updated = datetime.datetime( + 2024, 6, 15, tzinfo=pytz.utc + ) + self.recent_approved_submission.save() + + self.recent_rejected_submission = CommunityLibrarySubmission.objects.create( + channel=self.history_channel, + author=self.editor_user, + channel_version=1, + status=community_library_submission_constants.STATUS_REJECTED, + date_created=datetime.datetime(2024, 7, 1, tzinfo=pytz.utc), + resolved_by=self.admin_user, + ) + self.recent_rejected_submission.date_updated = datetime.datetime( + 2024, 7, 10, tzinfo=pytz.utc + ) + self.recent_rejected_submission.save() + + self.very_recent_pending_submission = CommunityLibrarySubmission.objects.create( + channel=self.math_advanced_channel, + author=self.editor_user, + channel_version=1, + status=community_library_submission_constants.STATUS_PENDING, + date_created=datetime.datetime(2024, 12, 1, tzinfo=pytz.utc), + ) + self.very_recent_pending_submission.date_updated = datetime.datetime( + 2024, 12, 1, tzinfo=pytz.utc + ) + self.very_recent_pending_submission.save() + + def tearDown(self): + self.ensure_db_exists_patcher.stop() + super().tearDown() + + def test_filter_by_date_updated_gte(self): + """Test filtering submissions updated on or after a specific date""" + self.client.force_authenticate(user=self.editor_user) + response = self.client.get( + reverse_with_query( + "community-library-submission-list", + query={"date_updated__gte": "2024-06-01"}, + ) + ) + self.assertEqual(response.status_code, 200, response.content) + + results = response.data + self.assertEqual(len(results), 3) + submission_ids = [r["id"] for r in results] + self.assertNotIn(self.old_pending_submission.id, submission_ids) + + def test_filter_by_date_updated_lte(self): + """Test filtering submissions updated on or before a specific date""" + self.client.force_authenticate(user=self.editor_user) + response = self.client.get( + reverse_with_query( + "community-library-submission-list", + query={"date_updated__lte": "2024-06-30"}, + ) + ) + self.assertEqual(response.status_code, 200, response.content) + + results = response.data + self.assertEqual(len(results), 2) + submission_ids = [r["id"] for r in results] + self.assertIn(self.old_pending_submission.id, submission_ids) + self.assertIn(self.recent_approved_submission.id, submission_ids) + + def test_filter_by_date_updated_range(self): + """Test filtering submissions within a date range""" + self.client.force_authenticate(user=self.editor_user) + response = self.client.get( + reverse_with_query( + "community-library-submission-list", + query={ + "date_updated__gte": "2024-06-01", + "date_updated__lte": "2024-07-31", + }, + ) + ) + self.assertEqual(response.status_code, 200, response.content) + + results = response.data + self.assertEqual(len(results), 2) + submission_ids = [r["id"] for r in results] + self.assertIn(self.recent_approved_submission.id, submission_ids) + self.assertIn(self.recent_rejected_submission.id, submission_ids) + + def test_filter_by_status_in_single(self): + """Test filtering by a single status""" + self.client.force_authenticate(user=self.editor_user) + response = self.client.get( + reverse_with_query( + "community-library-submission-list", + query={"status__in": community_library_submission_constants.STATUS_APPROVED}, + ) + ) + self.assertEqual(response.status_code, 200, response.content) + + results = response.data + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["id"], self.recent_approved_submission.id) + self.assertEqual( + results[0]["status"], + community_library_submission_constants.STATUS_APPROVED, + ) + + def test_filter_by_status_in_multiple(self): + """Test filtering by multiple statuses""" + self.client.force_authenticate(user=self.editor_user) + url = reverse("community-library-submission-list") + response = self.client.get( + f"{url}?status__in={community_library_submission_constants.STATUS_APPROVED}&status__in={community_library_submission_constants.STATUS_REJECTED}" + ) + self.assertEqual(response.status_code, 200, response.content) + + results = response.data + self.assertEqual(len(results), 2) + statuses = [r["status"] for r in results] + self.assertIn( + community_library_submission_constants.STATUS_APPROVED, statuses + ) + self.assertIn( + community_library_submission_constants.STATUS_REJECTED, statuses + ) + self.assertNotIn( + community_library_submission_constants.STATUS_PENDING, statuses + ) + + def test_search_by_channel_name(self): + """Test searching submissions by channel name""" + self.client.force_authenticate(user=self.editor_user) + response = self.client.get( + reverse_with_query( + "community-library-submission-list", + query={"search": "Math"}, + ) + ) + self.assertEqual(response.status_code, 200, response.content) + + results = response.data + self.assertEqual(len(results), 2) + channel_ids = [r["channel_id"] for r in results] + self.assertIn(self.math_channel.id, channel_ids) + self.assertIn(self.math_advanced_channel.id, channel_ids) + + def test_search_by_channel_name_partial(self): + """Test searching with partial channel name""" + self.client.force_authenticate(user=self.editor_user) + response = self.client.get( + reverse_with_query( + "community-library-submission-list", + query={"search": "Science"}, + ) + ) + self.assertEqual(response.status_code, 200, response.content) + + results = response.data + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["channel_id"], self.science_channel.id) + + def test_combined_filters(self): + """Test combining multiple filters together""" + self.client.force_authenticate(user=self.editor_user) + response = self.client.get( + reverse_with_query( + "community-library-submission-list", + query={ + "date_updated__gte": "2024-01-01", + "status__in": community_library_submission_constants.STATUS_PENDING, + "search": "Math", + }, + ) + ) + self.assertEqual(response.status_code, 200, response.content) + + results = response.data + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["id"], self.very_recent_pending_submission.id) + self.assertEqual( + results[0]["status"], + community_library_submission_constants.STATUS_PENDING, + ) + self.assertEqual(results[0]["channel_id"], self.math_advanced_channel.id) + + def test_resolved_by_name_visible_to_editor(self): + """Test that editors can now see who resolved their submissions""" + self.client.force_authenticate(user=self.editor_user) + response = self.client.get( + reverse( + "community-library-submission-detail", + args=[self.recent_approved_submission.id], + ), + format="json", + ) + self.assertEqual(response.status_code, 200, response.content) + + result = response.data + self.assertIn("resolved_by_id", result) + self.assertIn("resolved_by_name", result) + self.assertEqual(result["resolved_by_id"], self.admin_user.id) + self.assertEqual( + result["resolved_by_name"], + f"{self.admin_user.first_name} {self.admin_user.last_name}", + ) + + def test_resolved_by_name_null_for_unresolved(self): + """Test that resolved_by_name is None for unresolved submissions""" + self.client.force_authenticate(user=self.editor_user) + response = self.client.get( + reverse( + "community-library-submission-detail", + args=[self.old_pending_submission.id], + ), + format="json", + ) + self.assertEqual(response.status_code, 200, response.content) + + result = response.data + self.assertIsNone(result.get("resolved_by_id")) + self.assertIsNone(result.get("resolved_by_name")) + + def test_pagination_ordering_by_date_updated(self): + """Test that results are ordered by -date_updated (most recent first)""" + self.client.force_authenticate(user=self.editor_user) + response = self.client.get( + reverse("community-library-submission-list"), + format="json", + ) + self.assertEqual(response.status_code, 200, response.content) + + results = response.data + self.assertEqual(len(results), 4) + self.assertEqual(results[0]["id"], self.very_recent_pending_submission.id) + self.assertEqual(results[3]["id"], self.old_pending_submission.id) + + def test_admin_can_use_filters(self): + """Test that admin users can also use the new filters""" + self.client.force_authenticate(user=self.admin_user) + response = self.client.get( + reverse_with_query( + "admin-community-library-submission-list", + query={ + "date_updated__gte": "2024-06-01", + "status__in": community_library_submission_constants.STATUS_APPROVED, + }, + ) + ) + self.assertEqual(response.status_code, 200, response.content) + + results = response.data + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["id"], self.recent_approved_submission.id) diff --git a/contentcuration/contentcuration/viewsets/community_library_submission.py b/contentcuration/contentcuration/viewsets/community_library_submission.py index abaf1c8521..f200570d60 100644 --- a/contentcuration/contentcuration/viewsets/community_library_submission.py +++ b/contentcuration/contentcuration/viewsets/community_library_submission.py @@ -1,6 +1,10 @@ from django.utils import timezone +import django_filters +from django_filters import DateTimeFilter +from django_filters import MultipleChoiceFilter from django_filters.rest_framework import DjangoFilterBackend from rest_framework.decorators import action +from rest_framework.filters import SearchFilter from rest_framework.permissions import IsAuthenticated from rest_framework.relations import PrimaryKeyRelatedField from rest_framework.response import Response @@ -28,6 +32,22 @@ from contentcuration.viewsets.user import IsAdminUser +class CommunityLibrarySubmissionFilterSet(django_filters.FilterSet): + """ + FilterSet for CommunityLibrarySubmission to support notifications page filtering. + """ + date_updated__lte = DateTimeFilter(field_name='date_updated', lookup_expr='lte') + date_updated__gte = DateTimeFilter(field_name='date_updated', lookup_expr='gte') + status__in = MultipleChoiceFilter( + field_name='status', + choices=community_library_submission_constants.status_choices, + ) + + class Meta: + model = CommunityLibrarySubmission + fields = ['channel', 'date_updated__lte', 'date_updated__gte', 'status__in'] + + class CommunityLibrarySubmissionSerializer(BulkModelSerializer): countries = PrimaryKeyRelatedField( many=True, @@ -163,7 +183,7 @@ def update(self, instance, validated_data): class CommunityLibrarySubmissionPagination(ValuesViewsetCursorPagination): - ordering = "-date_created" + ordering = "-date_updated" page_size_query_param = "max_results" max_page_size = 100 @@ -172,6 +192,14 @@ def get_author_name(item): return "{} {}".format(item["author__first_name"], item["author__last_name"]) +def get_resolved_by_name(item): + if item.get("resolved_by__first_name") and item.get("resolved_by__last_name"): + return "{} {}".format( + item["resolved_by__first_name"], item["resolved_by__last_name"] + ) + return None + + class CommunityLibrarySubmissionViewSetMixin: """ Mixin with logic shared between the CommunityLibrarySubmissionViewSet and @@ -191,14 +219,19 @@ class CommunityLibrarySubmissionViewSetMixin: "status", "resolution_reason", "feedback_notes", - "date_resolved", + "date_updated", + "resolved_by_id", + "resolved_by__first_name", + "resolved_by__last_name", ) field_map = { "author_name": get_author_name, + "resolved_by_name": get_resolved_by_name, } - queryset = CommunityLibrarySubmission.objects.all().order_by("-date_created") - filter_backends = [DjangoFilterBackend] - filterset_fields = ["channel"] + queryset = CommunityLibrarySubmission.objects.all().order_by("-date_updated") + filter_backends = [DjangoFilterBackend, SearchFilter] + filterset_class = CommunityLibrarySubmissionFilterSet + search_fields = ["channel__name"] pagination_class = CommunityLibrarySubmissionPagination def consolidate(self, items, queryset): @@ -227,12 +260,6 @@ class CommunityLibrarySubmissionViewSet( serializer_class = CommunityLibrarySubmissionSerializer -def get_resolved_by_name(item): - return "{} {}".format( - item["resolved_by__first_name"], item["resolved_by__last_name"] - ) - - class AdminCommunityLibrarySubmissionViewSet( CommunityLibrarySubmissionViewSetMixin, ReadOnlyValuesViewset, @@ -240,17 +267,9 @@ class AdminCommunityLibrarySubmissionViewSet( permission_classes = [IsAdminUser] values = CommunityLibrarySubmissionViewSetMixin.values + ( - "resolved_by_id", - "resolved_by__first_name", - "resolved_by__last_name", "internal_notes", ) field_map = CommunityLibrarySubmissionViewSetMixin.field_map.copy() - field_map.update( - { - "resolved_by_name": get_resolved_by_name, - } - ) def _mark_previous_pending_submissions_as_superseded(self, submission): CommunityLibrarySubmission.objects.filter( @@ -288,9 +307,9 @@ def resolve(self, request, pk=None): serializer = self.get_serializer(instance, data=request.data, partial=True) serializer.is_valid(raise_exception=True) - date_resolved = timezone.now() + date_updated = timezone.now() submission = serializer.save( - date_resolved=date_resolved, + date_updated=date_updated, resolved_by=request.user, ) From 3023a5af2cd5531a36d6ca5d5300f7a4bca6c988 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sun, 23 Nov 2025 17:38:47 +0000 Subject: [PATCH 02/16] [pre-commit.ci lite] apply automatic fixes --- .../migrations/0158_auto_20251122_2351.py | 41 ++++++++++++------- .../test_community_library_submission.py | 12 +++--- .../viewsets/community_library_submission.py | 15 ++++--- 3 files changed, 39 insertions(+), 29 deletions(-) diff --git a/contentcuration/contentcuration/migrations/0158_auto_20251122_2351.py b/contentcuration/contentcuration/migrations/0158_auto_20251122_2351.py index 3d12278325..d99b84f0c7 100644 --- a/contentcuration/contentcuration/migrations/0158_auto_20251122_2351.py +++ b/contentcuration/contentcuration/migrations/0158_auto_20251122_2351.py @@ -1,35 +1,48 @@ # Generated by Django 3.2.24 on 2025-11-22 23:51 - -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): dependencies = [ - ('contentcuration', '0157_merge_20251015_0333'), + ("contentcuration", "0157_merge_20251015_0333"), ] operations = [ migrations.RemoveIndex( - model_name='communitylibrarysubmission', - name='submission_date_created_idx', + model_name="communitylibrarysubmission", + name="submission_date_created_idx", ), migrations.RemoveField( - model_name='communitylibrarysubmission', - name='date_resolved', + model_name="communitylibrarysubmission", + name="date_resolved", ), migrations.AddField( - model_name='communitylibrarysubmission', - name='date_updated', + model_name="communitylibrarysubmission", + name="date_updated", field=models.DateTimeField(blank=True, db_index=True, null=True), ), migrations.AlterField( - model_name='communitylibrarysubmission', - name='status', - field=models.CharField(choices=[('PENDING', 'Pending'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected'), ('SUPERSEDED', 'Superseded'), ('LIVE', 'Live')], db_index=True, default='PENDING', max_length=20), + model_name="communitylibrarysubmission", + name="status", + field=models.CharField( + choices=[ + ("PENDING", "Pending"), + ("APPROVED", "Approved"), + ("REJECTED", "Rejected"), + ("SUPERSEDED", "Superseded"), + ("LIVE", "Live"), + ], + db_index=True, + default="PENDING", + max_length=20, + ), ), migrations.AddIndex( - model_name='communitylibrarysubmission', - index=models.Index(fields=['-date_updated'], name='submission_date_updated_idx'), + model_name="communitylibrarysubmission", + index=models.Index( + fields=["-date_updated"], name="submission_date_updated_idx" + ), ), ] diff --git a/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py b/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py index 02b14ca01a..a6f4771ce9 100644 --- a/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py +++ b/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py @@ -1064,7 +1064,9 @@ def test_filter_by_status_in_single(self): response = self.client.get( reverse_with_query( "community-library-submission-list", - query={"status__in": community_library_submission_constants.STATUS_APPROVED}, + query={ + "status__in": community_library_submission_constants.STATUS_APPROVED + }, ) ) self.assertEqual(response.status_code, 200, response.content) @@ -1089,12 +1091,8 @@ def test_filter_by_status_in_multiple(self): results = response.data self.assertEqual(len(results), 2) statuses = [r["status"] for r in results] - self.assertIn( - community_library_submission_constants.STATUS_APPROVED, statuses - ) - self.assertIn( - community_library_submission_constants.STATUS_REJECTED, statuses - ) + self.assertIn(community_library_submission_constants.STATUS_APPROVED, statuses) + self.assertIn(community_library_submission_constants.STATUS_REJECTED, statuses) self.assertNotIn( community_library_submission_constants.STATUS_PENDING, statuses ) diff --git a/contentcuration/contentcuration/viewsets/community_library_submission.py b/contentcuration/contentcuration/viewsets/community_library_submission.py index f200570d60..4506147021 100644 --- a/contentcuration/contentcuration/viewsets/community_library_submission.py +++ b/contentcuration/contentcuration/viewsets/community_library_submission.py @@ -1,5 +1,5 @@ -from django.utils import timezone import django_filters +from django.utils import timezone from django_filters import DateTimeFilter from django_filters import MultipleChoiceFilter from django_filters.rest_framework import DjangoFilterBackend @@ -36,16 +36,17 @@ class CommunityLibrarySubmissionFilterSet(django_filters.FilterSet): """ FilterSet for CommunityLibrarySubmission to support notifications page filtering. """ - date_updated__lte = DateTimeFilter(field_name='date_updated', lookup_expr='lte') - date_updated__gte = DateTimeFilter(field_name='date_updated', lookup_expr='gte') + + date_updated__lte = DateTimeFilter(field_name="date_updated", lookup_expr="lte") + date_updated__gte = DateTimeFilter(field_name="date_updated", lookup_expr="gte") status__in = MultipleChoiceFilter( - field_name='status', + field_name="status", choices=community_library_submission_constants.status_choices, ) class Meta: model = CommunityLibrarySubmission - fields = ['channel', 'date_updated__lte', 'date_updated__gte', 'status__in'] + fields = ["channel", "date_updated__lte", "date_updated__gte", "status__in"] class CommunityLibrarySubmissionSerializer(BulkModelSerializer): @@ -266,9 +267,7 @@ class AdminCommunityLibrarySubmissionViewSet( ): permission_classes = [IsAdminUser] - values = CommunityLibrarySubmissionViewSetMixin.values + ( - "internal_notes", - ) + values = CommunityLibrarySubmissionViewSetMixin.values + ("internal_notes",) field_map = CommunityLibrarySubmissionViewSetMixin.field_map.copy() def _mark_previous_pending_submissions_as_superseded(self, submission): From 5b1f617da600d9cd12c2c6aff80bfc8d8edcc5f4 Mon Sep 17 00:00:00 2001 From: taoerman Date: Sun, 23 Nov 2025 09:55:29 -0800 Subject: [PATCH 03/16] fix linting --- ...51122_2351.py => 0158_rename_date_resolved_to_date_updated.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename contentcuration/contentcuration/migrations/{0158_auto_20251122_2351.py => 0158_rename_date_resolved_to_date_updated.py} (100%) diff --git a/contentcuration/contentcuration/migrations/0158_auto_20251122_2351.py b/contentcuration/contentcuration/migrations/0158_rename_date_resolved_to_date_updated.py similarity index 100% rename from contentcuration/contentcuration/migrations/0158_auto_20251122_2351.py rename to contentcuration/contentcuration/migrations/0158_rename_date_resolved_to_date_updated.py From 87817ceaf646f37cb8ee1634b851aa2c3a1a017d Mon Sep 17 00:00:00 2001 From: taoerman Date: Sun, 30 Nov 2025 15:32:39 -0800 Subject: [PATCH 04/16] fix code --- ...58_rename_date_resolved_to_date_updated.py | 48 -------- ...mmunity_library_submission_date_updated.py | 31 ++++++ contentcuration/contentcuration/models.py | 6 +- .../test_community_library_submission.py | 104 ++++++++++-------- .../viewsets/community_library_submission.py | 28 +++-- 5 files changed, 106 insertions(+), 111 deletions(-) delete mode 100644 contentcuration/contentcuration/migrations/0158_rename_date_resolved_to_date_updated.py create mode 100644 contentcuration/contentcuration/migrations/0158_update_community_library_submission_date_updated.py diff --git a/contentcuration/contentcuration/migrations/0158_rename_date_resolved_to_date_updated.py b/contentcuration/contentcuration/migrations/0158_rename_date_resolved_to_date_updated.py deleted file mode 100644 index d99b84f0c7..0000000000 --- a/contentcuration/contentcuration/migrations/0158_rename_date_resolved_to_date_updated.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 3.2.24 on 2025-11-22 23:51 -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - - dependencies = [ - ("contentcuration", "0157_merge_20251015_0333"), - ] - - operations = [ - migrations.RemoveIndex( - model_name="communitylibrarysubmission", - name="submission_date_created_idx", - ), - migrations.RemoveField( - model_name="communitylibrarysubmission", - name="date_resolved", - ), - migrations.AddField( - model_name="communitylibrarysubmission", - name="date_updated", - field=models.DateTimeField(blank=True, db_index=True, null=True), - ), - migrations.AlterField( - model_name="communitylibrarysubmission", - name="status", - field=models.CharField( - choices=[ - ("PENDING", "Pending"), - ("APPROVED", "Approved"), - ("REJECTED", "Rejected"), - ("SUPERSEDED", "Superseded"), - ("LIVE", "Live"), - ], - db_index=True, - default="PENDING", - max_length=20, - ), - ), - migrations.AddIndex( - model_name="communitylibrarysubmission", - index=models.Index( - fields=["-date_updated"], name="submission_date_updated_idx" - ), - ), - ] diff --git a/contentcuration/contentcuration/migrations/0158_update_community_library_submission_date_updated.py b/contentcuration/contentcuration/migrations/0158_update_community_library_submission_date_updated.py new file mode 100644 index 0000000000..90488a2a23 --- /dev/null +++ b/contentcuration/contentcuration/migrations/0158_update_community_library_submission_date_updated.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.24 on 2025-11-30 23:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contentcuration', '0157_merge_20251015_0333'), + ] + + operations = [ + migrations.RemoveIndex( + model_name='communitylibrarysubmission', + name='submission_date_created_idx', + ), + migrations.RemoveField( + model_name='communitylibrarysubmission', + name='date_resolved', + ), + migrations.AddField( + model_name='communitylibrarysubmission', + name='date_updated', + field=models.DateTimeField(auto_now=True, db_index=True), + ), + migrations.AlterField( + model_name='communitylibrarysubmission', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected'), ('SUPERSEDED', 'Superseded'), ('LIVE', 'Live')], db_index=True, default='PENDING', max_length=20), + ), + ] diff --git a/contentcuration/contentcuration/models.py b/contentcuration/contentcuration/models.py index e72b9d5fe2..23f637a147 100644 --- a/contentcuration/contentcuration/models.py +++ b/contentcuration/contentcuration/models.py @@ -2594,7 +2594,7 @@ class CommunityLibrarySubmission(models.Model): ) categories = models.JSONField(blank=True, null=True) date_created = models.DateTimeField(auto_now_add=True) - date_updated = models.DateTimeField(blank=True, null=True, db_index=True) + date_updated = models.DateTimeField(auto_now=True, db_index=True) status = models.CharField( max_length=20, choices=community_library_submission.status_choices, @@ -2703,10 +2703,6 @@ class Meta: name="unique_channel_with_channel_version", ), ] - indexes = [ - # Useful for cursor pagination - models.Index(fields=["-date_updated"], name="submission_date_updated_idx"), - ] ASSESSMENT_ID_INDEX_NAME = "assessment_id_idx" diff --git a/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py b/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py index a6f4771ce9..f9e219371b 100644 --- a/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py +++ b/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py @@ -504,6 +504,13 @@ def setUp(self): ) self.mock_datetime = self.datetime_patcher.start() + # Mock django.utils.timezone.now for auto_now field updates + self.django_timezone_patcher = mock.patch( + "django.utils.timezone.now", + return_value=self.resolved_time, + ) + self.django_timezone_patcher.start() + # Mock to allow creating submissions without having to set up content databases self.ensure_db_exists_patcher = mock.patch( "contentcuration.utils.publish.ensure_versioned_database_exists" @@ -551,8 +558,10 @@ def setUp(self): "internal_notes": self.internal_notes, } + def tearDown(self): self.datetime_patcher.stop() + self.django_timezone_patcher.stop() self.ensure_db_exists_patcher.stop() super().tearDown() @@ -564,7 +573,6 @@ def _manually_reject_submission(self): ) self.submission.feedback_notes = self.feedback_notes self.submission.internal_notes = self.internal_notes - self.submission.date_updated = self.resolved_time self.submission.save() def _refresh_submissions_from_db(self): @@ -951,55 +959,55 @@ def setUp(self): self.math_advanced_channel.editors.add(self.editor_user) self.math_advanced_channel.save() - self.old_pending_submission = CommunityLibrarySubmission.objects.create( - channel=self.math_channel, - author=self.editor_user, - channel_version=1, - status=community_library_submission_constants.STATUS_PENDING, - date_created=datetime.datetime(2023, 1, 1, tzinfo=pytz.utc), - ) - self.old_pending_submission.date_updated = datetime.datetime( - 2023, 1, 1, tzinfo=pytz.utc - ) - self.old_pending_submission.save() + with mock.patch( + "django.utils.timezone.now", + return_value=datetime.datetime(2023, 1, 1, tzinfo=pytz.utc), + ): + self.old_pending_submission = CommunityLibrarySubmission.objects.create( + channel=self.math_channel, + author=self.editor_user, + channel_version=1, + status=community_library_submission_constants.STATUS_PENDING, + date_created=datetime.datetime(2023, 1, 1, tzinfo=pytz.utc), + ) - self.recent_approved_submission = CommunityLibrarySubmission.objects.create( - channel=self.science_channel, - author=self.editor_user, - channel_version=1, - status=community_library_submission_constants.STATUS_APPROVED, - date_created=datetime.datetime(2024, 6, 1, tzinfo=pytz.utc), - resolved_by=self.admin_user, - ) - self.recent_approved_submission.date_updated = datetime.datetime( - 2024, 6, 15, tzinfo=pytz.utc - ) - self.recent_approved_submission.save() + with mock.patch( + "django.utils.timezone.now", + return_value=datetime.datetime(2024, 6, 15, tzinfo=pytz.utc), + ): + self.recent_approved_submission = CommunityLibrarySubmission.objects.create( + channel=self.science_channel, + author=self.editor_user, + channel_version=1, + status=community_library_submission_constants.STATUS_APPROVED, + date_created=datetime.datetime(2024, 6, 1, tzinfo=pytz.utc), + resolved_by=self.admin_user, + ) - self.recent_rejected_submission = CommunityLibrarySubmission.objects.create( - channel=self.history_channel, - author=self.editor_user, - channel_version=1, - status=community_library_submission_constants.STATUS_REJECTED, - date_created=datetime.datetime(2024, 7, 1, tzinfo=pytz.utc), - resolved_by=self.admin_user, - ) - self.recent_rejected_submission.date_updated = datetime.datetime( - 2024, 7, 10, tzinfo=pytz.utc - ) - self.recent_rejected_submission.save() + with mock.patch( + "django.utils.timezone.now", + return_value=datetime.datetime(2024, 7, 10, tzinfo=pytz.utc), + ): + self.recent_rejected_submission = CommunityLibrarySubmission.objects.create( + channel=self.history_channel, + author=self.editor_user, + channel_version=1, + status=community_library_submission_constants.STATUS_REJECTED, + date_created=datetime.datetime(2024, 7, 1, tzinfo=pytz.utc), + resolved_by=self.admin_user, + ) - self.very_recent_pending_submission = CommunityLibrarySubmission.objects.create( - channel=self.math_advanced_channel, - author=self.editor_user, - channel_version=1, - status=community_library_submission_constants.STATUS_PENDING, - date_created=datetime.datetime(2024, 12, 1, tzinfo=pytz.utc), - ) - self.very_recent_pending_submission.date_updated = datetime.datetime( - 2024, 12, 1, tzinfo=pytz.utc - ) - self.very_recent_pending_submission.save() + with mock.patch( + "django.utils.timezone.now", + return_value=datetime.datetime(2024, 12, 1, tzinfo=pytz.utc), + ): + self.very_recent_pending_submission = CommunityLibrarySubmission.objects.create( + channel=self.math_advanced_channel, + author=self.editor_user, + channel_version=1, + status=community_library_submission_constants.STATUS_PENDING, + date_created=datetime.datetime(2024, 12, 1, tzinfo=pytz.utc), + ) def tearDown(self): self.ensure_db_exists_patcher.stop() @@ -1084,7 +1092,7 @@ def test_filter_by_status_in_multiple(self): self.client.force_authenticate(user=self.editor_user) url = reverse("community-library-submission-list") response = self.client.get( - f"{url}?status__in={community_library_submission_constants.STATUS_APPROVED}&status__in={community_library_submission_constants.STATUS_REJECTED}" + f"{url}?status__in={community_library_submission_constants.STATUS_APPROVED},{community_library_submission_constants.STATUS_REJECTED}" ) self.assertEqual(response.status_code, 200, response.content) diff --git a/contentcuration/contentcuration/viewsets/community_library_submission.py b/contentcuration/contentcuration/viewsets/community_library_submission.py index 4506147021..053aa6769f 100644 --- a/contentcuration/contentcuration/viewsets/community_library_submission.py +++ b/contentcuration/contentcuration/viewsets/community_library_submission.py @@ -1,8 +1,9 @@ -import django_filters from django.utils import timezone -from django_filters import DateTimeFilter -from django_filters import MultipleChoiceFilter +from django_filters import BaseInFilter +from django_filters import ChoiceFilter +from django_filters.rest_framework import DateTimeFilter from django_filters.rest_framework import DjangoFilterBackend +from django_filters.rest_framework import FilterSet from rest_framework.decorators import action from rest_framework.filters import SearchFilter from rest_framework.permissions import IsAuthenticated @@ -32,14 +33,21 @@ from contentcuration.viewsets.user import IsAdminUser -class CommunityLibrarySubmissionFilterSet(django_filters.FilterSet): +class ChoiceInFilter(BaseInFilter, ChoiceFilter): + """ + Allows passing multiple statuses in a single query param like status="STATUS_A,STATUS_B" + """ + pass + + +class CommunityLibrarySubmissionFilterSet(FilterSet): """ FilterSet for CommunityLibrarySubmission to support notifications page filtering. """ date_updated__lte = DateTimeFilter(field_name="date_updated", lookup_expr="lte") date_updated__gte = DateTimeFilter(field_name="date_updated", lookup_expr="gte") - status__in = MultipleChoiceFilter( + status__in = ChoiceInFilter( field_name="status", choices=community_library_submission_constants.status_choices, ) @@ -194,10 +202,10 @@ def get_author_name(item): def get_resolved_by_name(item): - if item.get("resolved_by__first_name") and item.get("resolved_by__last_name"): + if item.get("resolved_by__first_name") or item.get("resolved_by__last_name"): return "{} {}".format( - item["resolved_by__first_name"], item["resolved_by__last_name"] - ) + item.get("resolved_by__first_name", ""), item.get("resolved_by__last_name", "") + ).strip() return None @@ -211,6 +219,7 @@ class CommunityLibrarySubmissionViewSetMixin: "id", "description", "channel_id", + "channel__name", "channel_version", "author_id", "author__first_name", @@ -228,6 +237,7 @@ class CommunityLibrarySubmissionViewSetMixin: field_map = { "author_name": get_author_name, "resolved_by_name": get_resolved_by_name, + "channel_name": lambda item: item.get("channel__name"), } queryset = CommunityLibrarySubmission.objects.all().order_by("-date_updated") filter_backends = [DjangoFilterBackend, SearchFilter] @@ -306,9 +316,7 @@ def resolve(self, request, pk=None): serializer = self.get_serializer(instance, data=request.data, partial=True) serializer.is_valid(raise_exception=True) - date_updated = timezone.now() submission = serializer.save( - date_updated=date_updated, resolved_by=request.user, ) From 46998a35bee835870fead1a92fbe0bf62020a92c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sun, 30 Nov 2025 23:35:27 +0000 Subject: [PATCH 05/16] [pre-commit.ci lite] apply automatic fixes --- ...mmunity_library_submission_date_updated.py | 35 ++++++++++++------- .../test_community_library_submission.py | 15 ++++---- .../viewsets/community_library_submission.py | 4 ++- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/contentcuration/contentcuration/migrations/0158_update_community_library_submission_date_updated.py b/contentcuration/contentcuration/migrations/0158_update_community_library_submission_date_updated.py index 90488a2a23..d2ccfb4c4b 100644 --- a/contentcuration/contentcuration/migrations/0158_update_community_library_submission_date_updated.py +++ b/contentcuration/contentcuration/migrations/0158_update_community_library_submission_date_updated.py @@ -1,31 +1,42 @@ # Generated by Django 3.2.24 on 2025-11-30 23:27 - -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): dependencies = [ - ('contentcuration', '0157_merge_20251015_0333'), + ("contentcuration", "0157_merge_20251015_0333"), ] operations = [ migrations.RemoveIndex( - model_name='communitylibrarysubmission', - name='submission_date_created_idx', + model_name="communitylibrarysubmission", + name="submission_date_created_idx", ), migrations.RemoveField( - model_name='communitylibrarysubmission', - name='date_resolved', + model_name="communitylibrarysubmission", + name="date_resolved", ), migrations.AddField( - model_name='communitylibrarysubmission', - name='date_updated', + model_name="communitylibrarysubmission", + name="date_updated", field=models.DateTimeField(auto_now=True, db_index=True), ), migrations.AlterField( - model_name='communitylibrarysubmission', - name='status', - field=models.CharField(choices=[('PENDING', 'Pending'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected'), ('SUPERSEDED', 'Superseded'), ('LIVE', 'Live')], db_index=True, default='PENDING', max_length=20), + model_name="communitylibrarysubmission", + name="status", + field=models.CharField( + choices=[ + ("PENDING", "Pending"), + ("APPROVED", "Approved"), + ("REJECTED", "Rejected"), + ("SUPERSEDED", "Superseded"), + ("LIVE", "Live"), + ], + db_index=True, + default="PENDING", + max_length=20, + ), ), ] diff --git a/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py b/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py index f9e219371b..2ccd3e2473 100644 --- a/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py +++ b/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py @@ -558,7 +558,6 @@ def setUp(self): "internal_notes": self.internal_notes, } - def tearDown(self): self.datetime_patcher.stop() self.django_timezone_patcher.stop() @@ -1001,12 +1000,14 @@ def setUp(self): "django.utils.timezone.now", return_value=datetime.datetime(2024, 12, 1, tzinfo=pytz.utc), ): - self.very_recent_pending_submission = CommunityLibrarySubmission.objects.create( - channel=self.math_advanced_channel, - author=self.editor_user, - channel_version=1, - status=community_library_submission_constants.STATUS_PENDING, - date_created=datetime.datetime(2024, 12, 1, tzinfo=pytz.utc), + self.very_recent_pending_submission = ( + CommunityLibrarySubmission.objects.create( + channel=self.math_advanced_channel, + author=self.editor_user, + channel_version=1, + status=community_library_submission_constants.STATUS_PENDING, + date_created=datetime.datetime(2024, 12, 1, tzinfo=pytz.utc), + ) ) def tearDown(self): diff --git a/contentcuration/contentcuration/viewsets/community_library_submission.py b/contentcuration/contentcuration/viewsets/community_library_submission.py index 053aa6769f..69135d9291 100644 --- a/contentcuration/contentcuration/viewsets/community_library_submission.py +++ b/contentcuration/contentcuration/viewsets/community_library_submission.py @@ -37,6 +37,7 @@ class ChoiceInFilter(BaseInFilter, ChoiceFilter): """ Allows passing multiple statuses in a single query param like status="STATUS_A,STATUS_B" """ + pass @@ -204,7 +205,8 @@ def get_author_name(item): def get_resolved_by_name(item): if item.get("resolved_by__first_name") or item.get("resolved_by__last_name"): return "{} {}".format( - item.get("resolved_by__first_name", ""), item.get("resolved_by__last_name", "") + item.get("resolved_by__first_name", ""), + item.get("resolved_by__last_name", ""), ).strip() return None From 7dfa2f838bb52fa38ad5fb42d63cddbd65bb8c07 Mon Sep 17 00:00:00 2001 From: taoerman Date: Sun, 30 Nov 2025 15:44:06 -0800 Subject: [PATCH 06/16] fix linting --- .../contentcuration/viewsets/community_library_submission.py | 1 - 1 file changed, 1 deletion(-) diff --git a/contentcuration/contentcuration/viewsets/community_library_submission.py b/contentcuration/contentcuration/viewsets/community_library_submission.py index 69135d9291..64dc7aa19d 100644 --- a/contentcuration/contentcuration/viewsets/community_library_submission.py +++ b/contentcuration/contentcuration/viewsets/community_library_submission.py @@ -1,4 +1,3 @@ -from django.utils import timezone from django_filters import BaseInFilter from django_filters import ChoiceFilter from django_filters.rest_framework import DateTimeFilter From 0ffa40748d7a056fc0ce9b2d3eed9b75de32a144 Mon Sep 17 00:00:00 2001 From: taoerman Date: Sun, 30 Nov 2025 15:52:24 -0800 Subject: [PATCH 07/16] fix code --- ...=> 0159_update_community_library_submission_date_updated.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename contentcuration/contentcuration/migrations/{0158_update_community_library_submission_date_updated.py => 0159_update_community_library_submission_date_updated.py} (94%) diff --git a/contentcuration/contentcuration/migrations/0158_update_community_library_submission_date_updated.py b/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py similarity index 94% rename from contentcuration/contentcuration/migrations/0158_update_community_library_submission_date_updated.py rename to contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py index d2ccfb4c4b..4c20868963 100644 --- a/contentcuration/contentcuration/migrations/0158_update_community_library_submission_date_updated.py +++ b/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ("contentcuration", "0157_merge_20251015_0333"), + ("contentcuration", "0158_add_audited_special_permissions_license"), ] operations = [ From ada4d7d0866b32379910b5d79789e06b6efc7844 Mon Sep 17 00:00:00 2001 From: taoerman Date: Sun, 30 Nov 2025 16:50:54 -0800 Subject: [PATCH 08/16] fix code --- ...mmunity_library_submission_date_updated.py | 29 ++++--------------- .../test_community_library_submission.py | 7 +---- 2 files changed, 6 insertions(+), 30 deletions(-) diff --git a/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py b/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py index 4c20868963..ee15285f08 100644 --- a/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py +++ b/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py @@ -6,37 +6,18 @@ class Migration(migrations.Migration): dependencies = [ - ("contentcuration", "0158_add_audited_special_permissions_license"), + ("contentcuration", "0157_merge_20251015_0333"), ] operations = [ - migrations.RemoveIndex( + migrations.RenameField( model_name="communitylibrarysubmission", - name="submission_date_created_idx", + old_name="date_resolved", + new_name="date_updated", ), - migrations.RemoveField( - model_name="communitylibrarysubmission", - name="date_resolved", - ), - migrations.AddField( + migrations.AlterField( model_name="communitylibrarysubmission", name="date_updated", field=models.DateTimeField(auto_now=True, db_index=True), ), - migrations.AlterField( - model_name="communitylibrarysubmission", - name="status", - field=models.CharField( - choices=[ - ("PENDING", "Pending"), - ("APPROVED", "Approved"), - ("REJECTED", "Rejected"), - ("SUPERSEDED", "Superseded"), - ("LIVE", "Live"), - ], - db_index=True, - default="PENDING", - max_length=20, - ), - ), ] diff --git a/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py b/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py index 2ccd3e2473..fabda0757d 100644 --- a/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py +++ b/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py @@ -498,11 +498,6 @@ def setUp(self): super().setUp() self.resolved_time = datetime.datetime(2023, 10, 1, tzinfo=pytz.utc) - self.datetime_patcher = mock.patch( - "contentcuration.viewsets.community_library_submission.timezone.now", - return_value=self.resolved_time, - ) - self.mock_datetime = self.datetime_patcher.start() # Mock django.utils.timezone.now for auto_now field updates self.django_timezone_patcher = mock.patch( @@ -558,8 +553,8 @@ def setUp(self): "internal_notes": self.internal_notes, } + def tearDown(self): - self.datetime_patcher.stop() self.django_timezone_patcher.stop() self.ensure_db_exists_patcher.stop() super().tearDown() From 4d6edf26428a25f8dfdb23047749eec5ad6809af Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 00:53:23 +0000 Subject: [PATCH 09/16] [pre-commit.ci lite] apply automatic fixes --- .../tests/viewsets/test_community_library_submission.py | 1 - 1 file changed, 1 deletion(-) diff --git a/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py b/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py index fabda0757d..d8a07e94e6 100644 --- a/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py +++ b/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py @@ -553,7 +553,6 @@ def setUp(self): "internal_notes": self.internal_notes, } - def tearDown(self): self.django_timezone_patcher.stop() self.ensure_db_exists_patcher.stop() From d6efceaf46b14602e95aa90d74751d929eb136f8 Mon Sep 17 00:00:00 2001 From: taoerman Date: Sun, 30 Nov 2025 16:57:19 -0800 Subject: [PATCH 10/16] fix code --- ...add_audited_special_permissions_license.py | 39 +++++++++++++++++++ ...mmunity_library_submission_date_updated.py | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 contentcuration/contentcuration/migrations/0158_add_audited_special_permissions_license.py diff --git a/contentcuration/contentcuration/migrations/0158_add_audited_special_permissions_license.py b/contentcuration/contentcuration/migrations/0158_add_audited_special_permissions_license.py new file mode 100644 index 0000000000..8a74da76c3 --- /dev/null +++ b/contentcuration/contentcuration/migrations/0158_add_audited_special_permissions_license.py @@ -0,0 +1,39 @@ +# Generated by Django 3.2.24 on 2025-11-05 22:17 +import uuid + +from django.db import migrations +from django.db import models + +import contentcuration.models + + +class Migration(migrations.Migration): + + dependencies = [ + ("contentcuration", "0157_merge_20251015_0333"), + ] + + operations = [ + migrations.CreateModel( + name="AuditedSpecialPermissionsLicense", + fields=[ + ( + "id", + contentcuration.models.UUIDField( + default=uuid.uuid4, + max_length=32, + primary_key=True, + serialize=False, + ), + ), + ("description", models.TextField(db_index=True, unique=True)), + ("distributable", models.BooleanField(default=False)), + ], + ), + migrations.AddIndex( + model_name="auditedspecialpermissionslicense", + index=models.Index( + fields=["description"], name="audited_special_perms_desc_idx" + ), + ), + ] diff --git a/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py b/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py index ee15285f08..9720cba7ad 100644 --- a/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py +++ b/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ("contentcuration", "0157_merge_20251015_0333"), + ("contentcuration", "0158_add_audited_special_permissions_license"), ] operations = [ From 2d156b24ab140580052e534a89cbeec43d63fff7 Mon Sep 17 00:00:00 2001 From: taoerman Date: Sun, 30 Nov 2025 17:02:37 -0800 Subject: [PATCH 11/16] fix code --- .../0159_update_community_library_submission_date_updated.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py b/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py index 9720cba7ad..f3440bac87 100644 --- a/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py +++ b/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py @@ -10,6 +10,10 @@ class Migration(migrations.Migration): ] operations = [ + migrations.RemoveIndex( + model_name="communitylibrarysubmission", + name="submission_date_created_idx", + ), migrations.RenameField( model_name="communitylibrarysubmission", old_name="date_resolved", From 3f49e210db2f9ce659ca1b1d3915464f367bdf42 Mon Sep 17 00:00:00 2001 From: taoerman Date: Sun, 30 Nov 2025 17:13:00 -0800 Subject: [PATCH 12/16] fix code --- .../0159_update_community_library_submission_date_updated.py | 1 + 1 file changed, 1 insertion(+) diff --git a/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py b/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py index f3440bac87..26f8658a50 100644 --- a/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py +++ b/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py @@ -8,6 +8,7 @@ class Migration(migrations.Migration): dependencies = [ ("contentcuration", "0158_add_audited_special_permissions_license"), ] + operations = [ migrations.RemoveIndex( From 6b0f56f4dc43796faeb8799c98213cb6929101d1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 01:15:31 +0000 Subject: [PATCH 13/16] [pre-commit.ci lite] apply automatic fixes --- .../0159_update_community_library_submission_date_updated.py | 1 - 1 file changed, 1 deletion(-) diff --git a/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py b/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py index 26f8658a50..f3440bac87 100644 --- a/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py +++ b/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py @@ -8,7 +8,6 @@ class Migration(migrations.Migration): dependencies = [ ("contentcuration", "0158_add_audited_special_permissions_license"), ] - operations = [ migrations.RemoveIndex( From 569eae4ec26f76120db650971fd2e34b25af383e Mon Sep 17 00:00:00 2001 From: taoerman Date: Sun, 30 Nov 2025 17:17:23 -0800 Subject: [PATCH 14/16] fix code --- ..._alter_communitylibrarysubmission_status.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 contentcuration/contentcuration/migrations/0160_alter_communitylibrarysubmission_status.py diff --git a/contentcuration/contentcuration/migrations/0160_alter_communitylibrarysubmission_status.py b/contentcuration/contentcuration/migrations/0160_alter_communitylibrarysubmission_status.py new file mode 100644 index 0000000000..b3caa18f0f --- /dev/null +++ b/contentcuration/contentcuration/migrations/0160_alter_communitylibrarysubmission_status.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.24 on 2025-12-01 01:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contentcuration', '0159_update_community_library_submission_date_updated'), + ] + + operations = [ + migrations.AlterField( + model_name='communitylibrarysubmission', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected'), ('SUPERSEDED', 'Superseded'), ('LIVE', 'Live')], db_index=True, default='PENDING', max_length=20), + ), + ] From 176027302bc36a26ca72ef574b21cbfb312e21fd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 01:19:43 +0000 Subject: [PATCH 15/16] [pre-commit.ci lite] apply automatic fixes --- ...alter_communitylibrarysubmission_status.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/contentcuration/contentcuration/migrations/0160_alter_communitylibrarysubmission_status.py b/contentcuration/contentcuration/migrations/0160_alter_communitylibrarysubmission_status.py index b3caa18f0f..07b7261449 100644 --- a/contentcuration/contentcuration/migrations/0160_alter_communitylibrarysubmission_status.py +++ b/contentcuration/contentcuration/migrations/0160_alter_communitylibrarysubmission_status.py @@ -1,18 +1,29 @@ # Generated by Django 3.2.24 on 2025-12-01 01:16 - -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): dependencies = [ - ('contentcuration', '0159_update_community_library_submission_date_updated'), + ("contentcuration", "0159_update_community_library_submission_date_updated"), ] operations = [ migrations.AlterField( - model_name='communitylibrarysubmission', - name='status', - field=models.CharField(choices=[('PENDING', 'Pending'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected'), ('SUPERSEDED', 'Superseded'), ('LIVE', 'Live')], db_index=True, default='PENDING', max_length=20), + model_name="communitylibrarysubmission", + name="status", + field=models.CharField( + choices=[ + ("PENDING", "Pending"), + ("APPROVED", "Approved"), + ("REJECTED", "Rejected"), + ("SUPERSEDED", "Superseded"), + ("LIVE", "Live"), + ], + db_index=True, + default="PENDING", + max_length=20, + ), ), ] From baf0e76a43e96088f994eb6f3a540a68389768ce Mon Sep 17 00:00:00 2001 From: taoerman Date: Mon, 1 Dec 2025 10:05:09 -0800 Subject: [PATCH 16/16] merge two migrations --- ...mmunity_library_submission_date_updated.py | 16 ++++++++++ ...alter_communitylibrarysubmission_status.py | 29 ------------------- 2 files changed, 16 insertions(+), 29 deletions(-) delete mode 100644 contentcuration/contentcuration/migrations/0160_alter_communitylibrarysubmission_status.py diff --git a/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py b/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py index f3440bac87..ac8bd75777 100644 --- a/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py +++ b/contentcuration/contentcuration/migrations/0159_update_community_library_submission_date_updated.py @@ -24,4 +24,20 @@ class Migration(migrations.Migration): name="date_updated", field=models.DateTimeField(auto_now=True, db_index=True), ), + migrations.AlterField( + model_name="communitylibrarysubmission", + name="status", + field=models.CharField( + choices=[ + ("PENDING", "Pending"), + ("APPROVED", "Approved"), + ("REJECTED", "Rejected"), + ("SUPERSEDED", "Superseded"), + ("LIVE", "Live"), + ], + db_index=True, + default="PENDING", + max_length=20, + ), + ), ] diff --git a/contentcuration/contentcuration/migrations/0160_alter_communitylibrarysubmission_status.py b/contentcuration/contentcuration/migrations/0160_alter_communitylibrarysubmission_status.py deleted file mode 100644 index 07b7261449..0000000000 --- a/contentcuration/contentcuration/migrations/0160_alter_communitylibrarysubmission_status.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 3.2.24 on 2025-12-01 01:16 -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - - dependencies = [ - ("contentcuration", "0159_update_community_library_submission_date_updated"), - ] - - operations = [ - migrations.AlterField( - model_name="communitylibrarysubmission", - name="status", - field=models.CharField( - choices=[ - ("PENDING", "Pending"), - ("APPROVED", "Approved"), - ("REJECTED", "Rejected"), - ("SUPERSEDED", "Superseded"), - ("LIVE", "Live"), - ], - db_index=True, - default="PENDING", - max_length=20, - ), - ), - ]