Skip to content

Commit fc27318

Browse files
authored
Merge pull request #5233 from Jakoma02/ensure-channel-version-database-exists
Ensure channel version database exists when adding to community library
2 parents dd85e9e + 15d40ff commit fc27318

File tree

13 files changed

+431
-85
lines changed

13 files changed

+431
-85
lines changed

contentcuration/contentcuration/models.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2601,6 +2601,9 @@ class CommunityLibrarySubmission(models.Model):
26012601
internal_notes = models.TextField(blank=True, null=True)
26022602

26032603
def save(self, *args, **kwargs):
2604+
# Not a top-level import to avoid circular import issues
2605+
from contentcuration.tasks import ensure_versioned_database_exists_task
2606+
26042607
# Validate on save that the submission author is an editor of the channel
26052608
# and that the version is not greater than the current channel version.
26062609
# These cannot be expressed as constraints because traversing
@@ -2623,6 +2626,16 @@ def save(self, *args, **kwargs):
26232626
code="impossibly_high_channel_version",
26242627
)
26252628

2629+
if self.pk is None:
2630+
# When creating a new submission, ensure the channel has a versioned database
2631+
# (it might not have if the channel was published before versioned databases
2632+
# were introduced).
2633+
ensure_versioned_database_exists_task.fetch_or_enqueue(
2634+
user=self.author,
2635+
channel_id=self.channel.id,
2636+
channel_version=self.channel.version,
2637+
)
2638+
26262639
super().save(*args, **kwargs)
26272640

26282641
def mark_live(self):

contentcuration/contentcuration/tasks.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from contentcuration.utils.csv_writer import write_user_csv
2020
from contentcuration.utils.nodes import calculate_resource_size
2121
from contentcuration.utils.nodes import generate_diff
22+
from contentcuration.utils.publish import ensure_versioned_database_exists
2223
from contentcuration.viewsets.user import AdminUserFilter
2324

2425

@@ -159,3 +160,8 @@ def sendcustomemails_task(subject, message, query):
159160
text,
160161
settings.DEFAULT_FROM_EMAIL,
161162
)
163+
164+
165+
@app.task(name="ensure_versioned_database_exists_task")
166+
def ensure_versioned_database_exists_task(channel_id, channel_version):
167+
ensure_versioned_database_exists(channel_id, channel_version)

contentcuration/contentcuration/tests/helpers.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django_celery_results.models import TaskResult
88
from search.models import ContentNodeFullTextSearch
99

10+
from contentcuration.celery import app
1011
from contentcuration.models import ContentNode
1112

1213

@@ -71,3 +72,27 @@ def reverse_with_query(
7172
if query:
7273
return f"{url}?{urlencode(query)}"
7374
return url
75+
76+
77+
class EagerTasksTestMixin(object):
78+
"""
79+
Mixin to make Celery tasks run synchronously during the tests.
80+
"""
81+
82+
celery_task_always_eager = None
83+
84+
@classmethod
85+
def setUpClass(cls):
86+
super().setUpClass()
87+
# update celery so tasks are always eager for this test, meaning they'll execute synchronously
88+
cls.celery_task_always_eager = app.conf.task_always_eager
89+
app.conf.update(task_always_eager=True)
90+
91+
def setUp(self):
92+
super().setUp()
93+
clear_tasks()
94+
95+
@classmethod
96+
def tearDownClass(cls):
97+
super().tearDownClass()
98+
app.conf.update(task_always_eager=cls.celery_task_always_eager)

contentcuration/contentcuration/tests/test_models.py

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from contentcuration.models import UserHistory
3535
from contentcuration.tests import testdata
3636
from contentcuration.tests.base import StudioTestCase
37+
from contentcuration.tests.helpers import EagerTasksTestMixin
3738
from contentcuration.viewsets.sync.constants import DELETED
3839

3940

@@ -549,12 +550,18 @@ def test_make_content_id_unique(self):
549550
self.assertNotEqual(copied_node_old_content_id, copied_node.content_id)
550551

551552

552-
class CommunityLibrarySubmissionTestCase(PermissionQuerysetTestCase):
553+
@mock.patch(
554+
"contentcuration.tasks.ensure_versioned_database_exists_task.fetch_or_enqueue",
555+
return_value=None,
556+
)
557+
class CommunityLibrarySubmissionTestCase(
558+
EagerTasksTestMixin, PermissionQuerysetTestCase
559+
):
553560
@property
554561
def base_queryset(self):
555562
return CommunityLibrarySubmission.objects.all()
556563

557-
def test_create_submission(self):
564+
def test_create_submission(self, mock_ensure_db_exists_task_fetch_or_enqueue):
558565
# Smoke test
559566
channel = testdata.channel()
560567
author = testdata.user()
@@ -577,51 +584,85 @@ def test_create_submission(self):
577584
submission.full_clean()
578585
submission.save()
579586

580-
def test_save__author_not_editor(self):
587+
def test_save__author_not_editor(self, mock_ensure_db_exists):
581588
submission = testdata.community_library_submission()
582589
user = testdata.user("[email protected]")
583590
submission.author = user
584591
with self.assertRaises(ValidationError):
585592
submission.save()
586593

587-
def test_save__nonpositive_channel_version(self):
594+
def test_save__nonpositive_channel_version(
595+
self, mock_ensure_db_exists_task_fetch_or_enqueue
596+
):
588597
submission = testdata.community_library_submission()
589598
submission.channel_version = 0
590599
with self.assertRaises(ValidationError):
591600
submission.save()
592601

593-
def test_save__matching_channel_version(self):
602+
def test_save__matching_channel_version(
603+
self, mock_ensure_db_exists_task_fetch_or_enqueue
604+
):
594605
submission = testdata.community_library_submission()
595606
submission.channel.version = 5
596607
submission.channel.save()
597608
submission.channel_version = 5
598609
submission.save()
599610

600-
def test_save__impossibly_high_channel_version(self):
611+
def test_save__impossibly_high_channel_version(
612+
self, mock_ensure_db_exists_task_fetch_or_enqueue
613+
):
601614
submission = testdata.community_library_submission()
602615
submission.channel.version = 5
603616
submission.channel.save()
604617
submission.channel_version = 6
605618
with self.assertRaises(ValidationError):
606619
submission.save()
607620

608-
def test_filter_view_queryset__anonymous(self):
621+
def test_save__ensure_versioned_database_exists_on_create(
622+
self, mock_ensure_db_exists_task_fetch_or_enqueue
623+
):
624+
submission = testdata.community_library_submission()
625+
626+
mock_ensure_db_exists_task_fetch_or_enqueue.assert_called_once_with(
627+
user=submission.author,
628+
channel_id=submission.channel.id,
629+
channel_version=submission.channel.version,
630+
)
631+
632+
def test_save__dont_ensure_versioned_database_exists_on_update(
633+
self, mock_ensure_db_exists_task_fetch_or_enqueue
634+
):
635+
submission = testdata.community_library_submission()
636+
mock_ensure_db_exists_task_fetch_or_enqueue.reset_mock()
637+
638+
submission.description = "Updated description"
639+
submission.save()
640+
641+
mock_ensure_db_exists_task_fetch_or_enqueue.assert_not_called()
642+
643+
def test_filter_view_queryset__anonymous(
644+
self, mock_ensure_db_exists_task_fetch_or_enqueue
645+
):
609646
_ = testdata.community_library_submission()
610647

611648
queryset = CommunityLibrarySubmission.filter_view_queryset(
612649
self.base_queryset, user=self.anonymous_user
613650
)
614651
self.assertFalse(queryset.exists())
615652

616-
def test_filter_view_queryset__forbidden_user(self):
653+
def test_filter_view_queryset__forbidden_user(
654+
self, mock_ensure_db_exists_task_fetch_or_enqueue
655+
):
617656
_ = testdata.community_library_submission()
618657

619658
queryset = CommunityLibrarySubmission.filter_view_queryset(
620659
self.base_queryset, user=self.forbidden_user
621660
)
622661
self.assertFalse(queryset.exists())
623662

624-
def test_filter_view_queryset__channel_editor(self):
663+
def test_filter_view_queryset__channel_editor(
664+
self, mock_ensure_db_exists_task_fetch_or_enqueue
665+
):
625666
submission_a = testdata.community_library_submission()
626667
submission_b = testdata.community_library_submission()
627668

@@ -635,31 +676,39 @@ def test_filter_view_queryset__channel_editor(self):
635676
self.assertQuerysetContains(queryset, pk=submission_a.id)
636677
self.assertQuerysetDoesNotContain(queryset, pk=submission_b.id)
637678

638-
def test_filter_view_queryset__admin(self):
679+
def test_filter_view_queryset__admin(
680+
self, mock_ensure_db_exists_task_fetch_or_enqueue
681+
):
639682
submission_a = testdata.community_library_submission()
640683

641684
queryset = CommunityLibrarySubmission.filter_view_queryset(
642685
self.base_queryset, user=self.admin_user
643686
)
644687
self.assertQuerysetContains(queryset, pk=submission_a.id)
645688

646-
def test_filter_edit_queryset__anonymous(self):
689+
def test_filter_edit_queryset__anonymous(
690+
self, mock_ensure_db_exists_task_fetch_or_enqueue
691+
):
647692
_ = testdata.community_library_submission()
648693

649694
queryset = CommunityLibrarySubmission.filter_edit_queryset(
650695
self.base_queryset, user=self.anonymous_user
651696
)
652697
self.assertFalse(queryset.exists())
653698

654-
def test_filter_edit_queryset__forbidden_user(self):
699+
def test_filter_edit_queryset__forbidden_user(
700+
self, mock_ensure_db_exists_task_fetch_or_enqueue
701+
):
655702
_ = testdata.community_library_submission()
656703

657704
queryset = CommunityLibrarySubmission.filter_edit_queryset(
658705
self.base_queryset, user=self.forbidden_user
659706
)
660707
self.assertFalse(queryset.exists())
661708

662-
def test_filter_edit_queryset__channel_editor(self):
709+
def test_filter_edit_queryset__channel_editor(
710+
self, mock_ensure_db_exists_task_fetch_or_enqueue
711+
):
663712
submission = testdata.community_library_submission()
664713

665714
user = testdata.user()
@@ -671,7 +720,9 @@ def test_filter_edit_queryset__channel_editor(self):
671720
)
672721
self.assertFalse(queryset.exists())
673722

674-
def test_filter_edit_queryset__author(self):
723+
def test_filter_edit_queryset__author(
724+
self, mock_ensure_db_exists_task_fetch_or_enqueue
725+
):
675726
submission_a = testdata.community_library_submission()
676727
submission_b = testdata.community_library_submission()
677728

@@ -681,15 +732,17 @@ def test_filter_edit_queryset__author(self):
681732
self.assertQuerysetContains(queryset, pk=submission_a.id)
682733
self.assertQuerysetDoesNotContain(queryset, pk=submission_b.id)
683734

684-
def test_filter_edit_queryset__admin(self):
735+
def test_filter_edit_queryset__admin(
736+
self, mock_ensure_db_exists_task_fetch_or_enqueue
737+
):
685738
submission_a = testdata.community_library_submission()
686739

687740
queryset = CommunityLibrarySubmission.filter_edit_queryset(
688741
self.base_queryset, user=self.admin_user
689742
)
690743
self.assertQuerysetContains(queryset, pk=submission_a.id)
691744

692-
def test_mark_live(self):
745+
def test_mark_live(self, mock_ensure_db_exists_task_fetch_or_enqueue):
693746
submission_a = testdata.community_library_submission()
694747
submission_b = testdata.community_library_submission()
695748

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from django.core.files.storage import FileSystemStorage
2+
3+
4+
class RestrictedFileSystemStorage:
5+
"""
6+
A wrapper around FileSystemStorage that is restricted to more closely match
7+
the behavior of S3Storage which is used in production. In particuler,
8+
it does not expose the `path` method, and opening files for writing
9+
is not allowed.
10+
This cannot be solved by just mocking the `path` method, because
11+
it is used by the `FileSystemStorage` class internally.
12+
"""
13+
14+
def __init__(self, *args, **kwargs):
15+
self._inner = FileSystemStorage(*args, **kwargs)
16+
17+
def __getattr__(self, name):
18+
if name == "path":
19+
raise NotImplementedError(
20+
"The 'path' method is intentionally not available."
21+
)
22+
return getattr(self._inner, name)
23+
24+
def open(self, name, mode="rb"):
25+
if "w" in mode:
26+
raise ValueError(
27+
"Opening files for writing will not be available in production."
28+
)
29+
return self._inner.open(name, mode)
30+
31+
def __dir__(self):
32+
return [x for x in dir(self._inner) if x != "path"]

0 commit comments

Comments
 (0)