Skip to content

Commit d56669e

Browse files
committed
EDUCATOR-4638 | Add operating_user kwargs to DeferrableMixin.save()
1 parent 1fdd1e8 commit d56669e

File tree

5 files changed

+71
-35
lines changed

5 files changed

+71
-35
lines changed

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ Change Log
1414
Unreleased
1515
~~~~~~~~~~
1616

17+
[0.9.4] - 2019-09-24
18+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
19+
20+
* Let the ``DeferrableMixin.save()`` method take an optional ``operating_user`` parameter.
21+
1722
[0.9.3] - 2019-09-20
1823
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1924

super_csv/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44

55
from __future__ import absolute_import, unicode_literals
66

7-
__version__ = '0.9.3'
7+
__version__ = '0.9.4'
88

99
default_app_config = 'super_csv.apps.SuperCSVConfig' # pylint: disable=invalid-name

super_csv/mixins.py

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from celery_utils.logged_task import LoggedTask
1818
from crum import get_current_user
1919
from django.conf import settings
20-
from django.contrib.auth import get_user_model
2120
from django.db import DatabaseError, transaction
2221
from django.utils.translation import ugettext as _
2322
from six import text_type
@@ -81,13 +80,10 @@ class DeferrableMixin(object):
8180
Mixin that automatically commits data using celery.
8281

8382
Subclasses should specify `size_to_defer` to tune when to
84-
run the commit synchronously or asynchronously
83+
run the commit synchronously or asynchronously.
8584

8685
Subclasses must override get_unique_path to uniquely identify
87-
this task
88-
89-
Subclasses can define a field `user_id` which will be loaded and saved in the CSVOperation.
90-
Otherwise, the current request (if any) user will be used.
86+
this task.
9187
"""
9288
# if the number of rows is greater than size_to_defer,
9389
# run the task asynchonously. Otherwise, commit immediately.
@@ -97,9 +93,13 @@ class DeferrableMixin(object):
9793
def get_unique_path(self):
9894
raise NotImplementedError()
9995

100-
def save(self, op_name=None):
96+
def save(self, operation_name=None, operating_user=None):
10197
"""
10298
Save the state of this object to django storage.
99+
100+
Clients may pass an optional ``operating_user`` kwarg to
101+
indicate the ``auth.User`` who is saving this operation state.
102+
Otherwise, the current request's (if any) user will be recorded.
103103
"""
104104
state = self.__dict__.copy()
105105
for k in list(state):
@@ -108,24 +108,20 @@ def save(self, op_name=None):
108108
del state[k]
109109
elif isinstance(v, set):
110110
state[k] = list(v)
111+
111112
state['__class__'] = (self.__class__.__module__, self.__class__.__name__)
112-
if not op_name:
113-
op_name = 'stage' if self.can_commit else 'commit'
114113

115-
user_id = state.get('user_id')
116-
if user_id:
117-
user = get_user_model().objects.get(id=user_id)
118-
else:
119-
user = get_current_user()
114+
if not operation_name:
115+
operation_name = 'stage' if self.can_commit else 'commit'
120116

121117
operation = CSVOperation.record_operation(
122-
self,
123-
self.get_unique_path(),
124-
op_name,
125-
json.dumps(state),
126-
original_filename=state.get('filename', ''),
127-
user=user,
128-
)
118+
self,
119+
self.get_unique_path(),
120+
operation_name,
121+
json.dumps(state),
122+
original_filename=state.get('filename', ''),
123+
user=operating_user or get_current_user(),
124+
)
129125
return operation
130126

131127
@classmethod

tests/test_csv.py

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717

1818
class DummyProcessor(csv_processor.CSVProcessor):
19+
"""
20+
Fixture class the inherits from CSVProcessor.
21+
"""
1922
max_file_size = 20
2023
columns = ['foo', 'bar']
2124
required_columns = ['foo', 'bar']
@@ -48,16 +51,44 @@ class DummyChecksumProcessor(csv_processor.ChecksumMixin, DummyProcessor):
4851

4952
class DummyDeferrableProcessor(csv_processor.DeferrableMixin, DummyProcessor):
5053
size_to_defer = 1
54+
test_set = set()
55+
56+
def get_unique_path(self):
57+
return 'test'
58+
59+
60+
USERNAME_FROM_SUBCLASS = 'user_specified_by_client'
61+
62+
63+
class DummyDeferrableProcessorSavingUser(csv_processor.DeferrableMixin, DummyProcessor):
64+
"""
65+
Fixture that inherits from DeferrableMixin and overrides a save() method,
66+
which calls the parent save() method with a non-null ``operating_user`` kwarg.
67+
"""
68+
size_to_defer = 1
69+
70+
def save(self, operation_name=None, operating_user=None):
71+
user = get_user_model().objects.get(username=USERNAME_FROM_SUBCLASS)
72+
return super(DummyDeferrableProcessorSavingUser, self).save(
73+
operation_name=operation_name,
74+
operating_user=user,
75+
)
5176

5277
def get_unique_path(self):
5378
return 'test'
5479

5580

5681
@ddt.ddt
5782
class CSVTestCase(TestCase):
58-
def setUp(self):
59-
super(CSVTestCase, self).setUp()
60-
self.dummy_csv = 'foo,bar\r\n1,1\r\n2,2\r\n'
83+
"""
84+
Test class for CSVProcessor and DeferrableMixin classes.
85+
"""
86+
@classmethod
87+
def setUpTestData(cls):
88+
super(CSVTestCase, cls).setUpTestData()
89+
cls.dummy_csv = 'foo,bar\r\n1,1\r\n2,2\r\n'
90+
cls.user = get_user_model().objects.create_user(username='testuser', password='12345')
91+
cls.user_from_subclass = get_user_model().objects.create(username=USERNAME_FROM_SUBCLASS, password='12345')
6192

6293
def tearDown(self):
6394
super(CSVTestCase, self).tearDown()
@@ -66,7 +97,7 @@ def tearDown(self):
6697
def test_write(self):
6798
buf = io.StringIO()
6899
processor = DummyProcessor(dummy_arg=True)
69-
assert processor.dummy_arg is True
100+
assert processor.dummy_arg is True # pylint: disable=no-member
70101
processor.write_file(buf)
71102
data = buf.getvalue()
72103
assert data == self.dummy_csv
@@ -158,14 +189,20 @@ def test_defer_too_small(self):
158189
assert operation is not None
159190

160191
@mock.patch('super_csv.mixins.get_current_user')
161-
def test_user(self, patch_get_user):
162-
user = get_user_model().objects.create_user(username='testuser', password='12345')
163-
buf = ContentFile(self.dummy_csv)
192+
def test_operating_user_is_recorded_from_request(self, patch_get_user):
164193
processor = DummyDeferrableProcessor()
165-
patch_get_user.side_effect = (user, None, None)
166-
processor.user_id = user.id
167-
processor.process_file(buf)
194+
patch_get_user.return_value = self.user
195+
processor.process_file(ContentFile(self.dummy_csv))
196+
csv_operations = models.CSVOperation.objects.all()
197+
assert len(csv_operations) == 3
198+
for csv_operation in csv_operations:
199+
assert csv_operation.user == self.user
200+
201+
@mock.patch('super_csv.mixins.get_current_user', return_value=None)
202+
def test_operating_user_is_recorded_by_subclass(self, patch_get_user): # pylint: disable=unused-argument
203+
processor = DummyDeferrableProcessorSavingUser()
204+
processor.process_file(ContentFile(self.dummy_csv))
168205
csv_operations = models.CSVOperation.objects.all()
169206
assert len(csv_operations) == 3
170207
for csv_operation in csv_operations:
171-
assert csv_operation.user == user
208+
assert csv_operation.user == self.user_from_subclass

tests/test_models.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66

77
from __future__ import absolute_import, unicode_literals
88

9-
from datetime import datetime, timedelta
10-
119
from django.conf import settings
1210
from django.test import TestCase
1311
from mock import patch

0 commit comments

Comments
 (0)