Skip to content

Commit 13668c1

Browse files
committed
Always require an actor_id to identify the user who caused a specific event in a channel's history.
1 parent 7227366 commit 13668c1

File tree

20 files changed

+170
-105
lines changed

20 files changed

+170
-105
lines changed

contentcuration/contentcuration/models.py

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,32 @@ def exists(self, *filters):
716716
return Exists(self.queryset().filter(*filters).values("user_id"))
717717

718718

719+
class ChannelModelQuerySet(models.QuerySet):
720+
def create(self, **kwargs):
721+
"""
722+
Create a new object with the given kwargs, saving it to the database
723+
and returning the created object.
724+
Overriding the Django default here to allow passing through the actor_id
725+
to register this event in the channel history.
726+
"""
727+
# Either allow the actor_id to be passed in, or read from a special attribute
728+
# on the queryset, this makes super calls to other methods easier to handle
729+
# without having to reimplement the entire method.
730+
actor_id = kwargs.pop("actor_id", getattr(self, "_actor_id", None))
731+
obj = self.model(**kwargs)
732+
self._for_write = True
733+
obj.save(force_insert=True, using=self.db, actor_id=actor_id)
734+
return obj
735+
736+
def get_or_create(self, defaults=None, **kwargs):
737+
self._actor_id = kwargs.pop("actor_id", None)
738+
return super().get_or_create(defaults, **kwargs)
739+
740+
def update_or_create(self, defaults=None, **kwargs):
741+
self._actor_id = kwargs.pop("actor_id", None)
742+
return super().update_or_create(defaults, **kwargs)
743+
744+
719745
class Channel(models.Model):
720746
""" Permissions come from association with organizations """
721747
id = UUIDField(primary_key=True, default=uuid.uuid4)
@@ -800,6 +826,8 @@ class Channel(models.Model):
800826
"version",
801827
])
802828

829+
objects = ChannelModelQuerySet.as_manager()
830+
803831
@classmethod
804832
def get_editable(cls, user, channel_id):
805833
return cls.filter_edit_queryset(cls.objects.all(), user).get(id=channel_id)
@@ -874,6 +902,10 @@ def get_resource_size(self):
874902
return files['resource_size'] or 0
875903

876904
def on_create(self):
905+
actor_id = getattr(self, "_actor_id", None)
906+
if actor_id is None:
907+
raise ValueError("No actor_id passed to save method")
908+
877909
if not self.content_defaults:
878910
self.content_defaults = DEFAULT_CONTENT_DEFAULTS
879911

@@ -905,7 +937,7 @@ def on_create(self):
905937
if self.public and (self.main_tree and self.main_tree.published):
906938
delete_public_channel_cache_keys()
907939

908-
def on_update(self):
940+
def on_update(self): # noqa C901
909941
from contentcuration.utils.user import calculate_user_storage
910942
original_values = self._field_updates.changed()
911943

@@ -929,14 +961,23 @@ def on_update(self):
929961
for editor in self.editors.all():
930962
calculate_user_storage(editor.pk)
931963

932-
# Delete db if channel has been deleted and mark as unpublished
933964
if "deleted" in original_values and not original_values["deleted"]:
934965
self.pending_editors.all().delete()
966+
# Delete db if channel has been deleted and mark as unpublished
935967
export_db_storage_path = os.path.join(settings.DB_ROOT, "{channel_id}.sqlite3".format(channel_id=self.id))
936968
if default_storage.exists(export_db_storage_path):
937969
default_storage.delete(export_db_storage_path)
938970
if self.main_tree:
939971
self.main_tree.published = False
972+
# mark the instance as deleted or recovered, if requested
973+
if "deleted" in original_values:
974+
user_id = getattr(self, "_actor_id", None)
975+
if user_id is None:
976+
raise ValueError("No actor_id passed to save method")
977+
if original_values["deleted"]:
978+
self.history.create(actor_id=user_id, action=channel_history.RECOVERY)
979+
else:
980+
self.history.create(actor_id=user_id, action=channel_history.DELETION)
940981

941982
if self.main_tree and self.main_tree._field_updates.changed():
942983
self.main_tree.save()
@@ -946,13 +987,20 @@ def on_update(self):
946987
delete_public_channel_cache_keys()
947988

948989
def save(self, *args, **kwargs):
949-
if self._state.adding:
990+
self._actor_id = kwargs.pop("actor_id", None)
991+
creating = self._state.adding
992+
if creating:
993+
if self._actor_id is None:
994+
raise ValueError("No actor_id passed to save method")
950995
self.on_create()
951996
else:
952997
self.on_update()
953998

954999
super(Channel, self).save(*args, **kwargs)
9551000

1001+
if creating:
1002+
self.history.create(actor_id=self._actor_id, action=channel_history.CREATION)
1003+
9561004
def get_thumbnail(self):
9571005
return get_channel_thumbnail(self)
9581006

@@ -996,24 +1044,11 @@ def make_public(self, bypass_signals=False):
9961044

9971045
return self
9981046

999-
def mark_created(self, user):
1000-
self.history.create(actor_id=to_pk(user), action=channel_history.CREATION)
1001-
10021047
def mark_publishing(self, user):
10031048
self.history.create(actor_id=to_pk(user), action=channel_history.PUBLICATION)
10041049
self.main_tree.publishing = True
10051050
self.main_tree.save()
10061051

1007-
def mark_deleted(self, user):
1008-
self.history.create(actor_id=to_pk(user), action=channel_history.DELETION)
1009-
self.deleted = True
1010-
self.save()
1011-
1012-
def mark_recovered(self, user):
1013-
self.history.create(actor_id=to_pk(user), action=channel_history.RECOVERY)
1014-
self.deleted = False
1015-
self.save()
1016-
10171052
@property
10181053
def deletion_history(self):
10191054
return self.history.filter(action=channel_history.DELETION)

contentcuration/contentcuration/tests/test_channel_model.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ def test_returns_date_newer_when_node_added(self):
145145
# add a new node
146146
node(
147147
parent=self.channel.main_tree,
148-
data={"node_id": "nodez", "title": "new child", "kind_id": "video",},
148+
data={"node_id": "nodez", "title": "new child", "kind_id": "video"},
149149
)
150150
# check that the returned date is newer
151151
assert self.channel.get_date_modified() > old_date
@@ -160,7 +160,7 @@ def setUp(self):
160160
super(GetAllChannelsTestCase, self).setUp()
161161

162162
# create 10 channels for comparison
163-
self.channels = mixer.cycle(10).blend(Channel)
163+
self.channels = [Channel.objects.create(actor_id=self.admin_user.id) for _ in range(10)]
164164

165165
def test_returns_all_channels_in_the_db(self):
166166
"""
@@ -179,9 +179,10 @@ class ChannelSetTestCase(BaseAPITestCase):
179179
def setUp(self):
180180
super(ChannelSetTestCase, self).setUp()
181181
self.channelset = mixer.blend(ChannelSet, editors=[self.user])
182-
self.channels = mixer.cycle(10).blend(
183-
Channel, secret_tokens=[self.channelset.secret_token], editors=[self.user]
184-
)
182+
self.channels = [Channel.objects.create(actor_id=self.user.id) for _ in range(10)]
183+
for chann in self.channels:
184+
chann.secret_tokens.add(self.channelset.secret_token)
185+
chann.editors.add(self.user)
185186

186187
def test_get_user_channel_sets(self):
187188
""" Make sure get_user_channel_sets returns the correct sets """
@@ -215,7 +216,7 @@ def test_channelset_deletion(self):
215216
def test_save_channels_to_token(self):
216217
""" Check endpoint will assign token to channels """
217218
token = self.channelset.secret_token
218-
channels = mixer.cycle(5).blend(Channel)
219+
channels = [Channel.objects.create(actor_id=self.user.id) for _ in range(5)]
219220
channels = Channel.objects.filter(
220221
pk__in=[c.pk for c in channels]
221222
) # Make this a queryset
@@ -274,7 +275,7 @@ class ChannelMetadataSaveTestCase(StudioTestCase):
274275

275276
def setUp(self):
276277
super(ChannelMetadataSaveTestCase, self).setUp()
277-
self.channels = mixer.cycle(5).blend(Channel)
278+
self.channels = [Channel.objects.create(actor_id=self.admin_user.id) for _ in range(5)]
278279
for c in self.channels:
279280
c.main_tree.changed = False
280281
c.main_tree.save()

contentcuration/contentcuration/tests/test_contentnodes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -876,7 +876,7 @@ def _setup_original_and_deriative_nodes(self):
876876

877877
# Setup derivative channel
878878
self.new_channel = Channel.objects.create(
879-
name="derivative of teschannel", source_id="lkajs"
879+
name="derivative of teschannel", source_id="lkajs", actor_id=self.admin_user.id
880880
)
881881
self.new_channel.save()
882882
self.new_channel.main_tree = self._create_empty_tree()

contentcuration/contentcuration/tests/test_exportchannel.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -493,20 +493,21 @@ def tearDown(self):
493493
clear_tasks()
494494

495495
def test_convert_channel_thumbnail_empty_thumbnail(self):
496-
channel = cc.Channel.objects.create()
496+
channel = cc.Channel.objects.create(actor_id=self.admin_user.id)
497497
self.assertEqual("", convert_channel_thumbnail(channel))
498498

499499
def test_convert_channel_thumbnail_static_thumbnail(self):
500-
channel = cc.Channel.objects.create(thumbnail="/static/kolibri_flapping_bird.png")
500+
channel = cc.Channel.objects.create(thumbnail="/static/kolibri_flapping_bird.png", actor_id=self.admin_user.id)
501501
self.assertEqual("", convert_channel_thumbnail(channel))
502502

503503
def test_convert_channel_thumbnail_encoding_valid(self):
504-
channel = cc.Channel.objects.create(thumbnail="/content/kolibri_flapping_bird.png", thumbnail_encoding={"base64": "flappy_bird"})
504+
channel = cc.Channel.objects.create(
505+
thumbnail="/content/kolibri_flapping_bird.png", thumbnail_encoding={"base64": "flappy_bird"}, actor_id=self.admin_user.id)
505506
self.assertEqual("flappy_bird", convert_channel_thumbnail(channel))
506507

507508
def test_convert_channel_thumbnail_encoding_invalid(self):
508509
with patch("contentcuration.utils.publish.get_thumbnail_encoding", return_value="this is a test"):
509-
channel = cc.Channel.objects.create(thumbnail="/content/kolibri_flapping_bird.png", thumbnail_encoding={})
510+
channel = cc.Channel.objects.create(thumbnail="/content/kolibri_flapping_bird.png", thumbnail_encoding={}, actor_id=self.admin_user.id)
510511
self.assertEqual("this is a test", convert_channel_thumbnail(channel))
511512

512513
def test_create_slideshow_manifest(self):
@@ -543,7 +544,7 @@ def tearDown(self):
543544
os.remove(self.output_db)
544545

545546
def test_nonexistent_prerequisites(self):
546-
channel = cc.Channel.objects.create()
547+
channel = cc.Channel.objects.create(actor_id=self.admin_user.id)
547548
node1 = cc.ContentNode.objects.create(kind_id="exercise", parent_id=channel.main_tree.pk, complete=True)
548549
exercise = cc.ContentNode.objects.create(kind_id="exercise", complete=True)
549550

@@ -554,7 +555,7 @@ def test_nonexistent_prerequisites(self):
554555
class ChannelExportPublishedData(StudioTestCase):
555556
def test_fill_published_fields(self):
556557
version_notes = description()
557-
channel = cc.Channel.objects.create()
558+
channel = cc.Channel.objects.create(actor_id=self.admin_user.id)
558559
channel.last_published
559560
fill_published_fields(channel, version_notes)
560561
self.assertTrue(channel.published_data)

contentcuration/contentcuration/tests/test_models.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ def test_filter_view_queryset__public_channel(self):
165165
def test_filter_view_queryset__public_channel__deleted(self):
166166
channel = self.public_channel
167167
channel.deleted = True
168-
channel.save()
168+
channel.save(actor_id=self.admin_user.id)
169169

170170
queryset = Channel.filter_view_queryset(self.base_queryset, user=self.forbidden_user)
171171
self.assertQuerysetDoesNotContain(queryset, pk=channel.id)
@@ -816,7 +816,7 @@ def _setup_user_related_data(self):
816816
user_b = self._create_user("b@tester.com")
817817

818818
# Create a sole editor non-public channel.
819-
sole_editor_channel = Channel.objects.create(name="sole-editor")
819+
sole_editor_channel = Channel.objects.create(name="sole-editor", actor_id=user_a.id)
820820
sole_editor_channel.editors.add(user_a)
821821

822822
# Create sole-editor channel nodes.
@@ -944,18 +944,20 @@ def setUp(self):
944944
self.channel = testdata.channel()
945945

946946
def test_mark_channel_created(self):
947-
self.assertEqual(0, self.channel.history.filter(action=channel_history.CREATION).count())
948-
self.channel.mark_created(self.admin_user)
949-
self.assertEqual(1, self.channel.history.filter(actor=self.admin_user, action=channel_history.CREATION).count())
947+
self.assertEqual(1, self.channel.history.filter(action=channel_history.CREATION).count())
950948

951949
def test_mark_channel_deleted(self):
952950
self.assertEqual(0, self.channel.deletion_history.count())
953-
self.channel.mark_deleted(self.admin_user)
951+
self.channel.deleted = True
952+
self.channel.save(actor_id=self.admin_user.id)
954953
self.assertEqual(1, self.channel.deletion_history.filter(actor=self.admin_user).count())
955954

956955
def test_mark_channel_recovered(self):
957956
self.assertEqual(0, self.channel.history.filter(actor=self.admin_user, action=channel_history.RECOVERY).count())
958-
self.channel.mark_recovered(self.admin_user)
957+
self.channel.deleted = True
958+
self.channel.save(actor_id=self.admin_user.id)
959+
self.channel.deleted = False
960+
self.channel.save(actor_id=self.admin_user.id)
959961
self.assertEqual(1, self.channel.history.filter(actor=self.admin_user, action=channel_history.RECOVERY).count())
960962

961963
def test_prune(self):
@@ -966,6 +968,7 @@ def test_prune(self):
966968
testdata.channel()
967969
]
968970
last_history_ids = []
971+
ChannelHistory.objects.all().delete()
969972

970973
self.assertEqual(0, ChannelHistory.objects.count())
971974

contentcuration/contentcuration/tests/test_restore_channel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def setUp(self, thumb_mock):
9393
self.version,
9494
self.last_updated,
9595
)
96-
self.channel, _ = create_channel(self.cursor_mock, self.id)
96+
self.channel, _ = create_channel(self.cursor_mock, self.id, self.admin_user)
9797

9898
def test_restore_channel_id(self):
9999
self.assertEqual(self.channel.id, self.id)

contentcuration/contentcuration/tests/test_serializers.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from django.db.models.query import QuerySet
44
from le_utils.constants import content_kinds
5+
from mock import Mock
56
from rest_framework import serializers
67

78
from .base import BaseAPITestCase
@@ -146,12 +147,15 @@ class Meta:
146147
nested_writes = True
147148

148149
def test_save__create(self):
150+
request = Mock()
151+
request.user = self.user
149152
s = self.ChannelSerializer(
150153
data=dict(
151154
name="New test channel",
152155
description="This is the best test channel",
153156
content_defaults=dict(author="Buster"),
154-
)
157+
),
158+
context=dict(request=request),
155159
)
156160

157161
self.assertTrue(s.is_valid())
@@ -167,7 +171,7 @@ def test_save__update(self):
167171
description="This is the best test channel",
168172
content_defaults=dict(author="Buster"),
169173
)
170-
c.save()
174+
c.save(actor_id=self.user.id)
171175

172176
s = self.ChannelSerializer(
173177
c, data=dict(content_defaults=dict(license="Special Permissions"))

contentcuration/contentcuration/tests/test_sync.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class SyncTestCase(StudioTestCase):
3737

3838
def setUp(self):
3939
super(SyncTestCase, self).setUpBase()
40-
self.derivative_channel = Channel.objects.create(name="testchannel")
40+
self.derivative_channel = Channel.objects.create(name="testchannel", actor_id=self.admin_user.id)
4141
self.channel.main_tree.copy_to(self.derivative_channel.main_tree)
4242
self.derivative_channel.main_tree.refresh_from_db()
4343
self.derivative_channel.save()

contentcuration/contentcuration/tests/testdata.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,8 @@ def tree(parent=None):
211211

212212

213213
def channel(name="testchannel"):
214-
channel = cc.Channel.objects.create(name=name)
214+
channel_creator = user()
215+
channel = cc.Channel.objects.create(name=name, actor_id=channel_creator.id)
215216
channel.save()
216217

217218
channel.main_tree = tree()

0 commit comments

Comments
 (0)