@@ -59,8 +59,8 @@ class BlobField(models.JSONField):
59
59
We do this to support a clear and maintainable implementation.
60
60
61
61
:param ingress_to: A string defining the path within the bucket to which direct uploads
62
- will be ingressed, if temporary_ingress=True . These uploaded files will be moved to their
63
- ultimate path in the store on save of the model.
62
+ will be ingressed. These uploaded files will be moved to their ultimate path in the store on save of the model.
63
+
64
64
:param accept_mimetype: A string passed to widgets, suitable for use in the `accept` attribute
65
65
of an html filepicker. This will allow you to narrow down, eg to `image/*` or an even more
66
66
specific mimetype. No validation is done at the field level that objects actually are of this
@@ -77,7 +77,7 @@ class BlobField(models.JSONField):
77
77
78
78
:param max_size_bytes: The maximum size in bytes of files that can be uploaded.
79
79
80
- :overwrite_mode: One of `OVERWRITE_MODES` determining the circumstances under which overwrite
80
+ :param overwrite_mode: One of `OVERWRITE_MODES` determining the circumstances under which overwrite
81
81
is allowed. overwrite mode behaves as follows:
82
82
- never: Never allows overwrite
83
83
- update: Only when updating an object
@@ -86,6 +86,10 @@ class BlobField(models.JSONField):
86
86
- add-versioned: Only when adding an object to a versioned bucket (again, for completeness)
87
87
- add-update: Always allow (ie when adding or updating an object)
88
88
- add-update-versioned: When adding or updating an object to a versioned bucket
89
+
90
+ :param on_change: A callable that will be executed on change of the field value. This will be called
91
+ on commit of the transaction (ie once any file upload is ingressed to its final location) and allows you,
92
+ for example, to dispatch a worker task to further process the uploaded blob.
89
93
"""
90
94
91
95
description = _ ("A GCP Cloud Storage object" )
@@ -99,6 +103,7 @@ def __init__(
99
103
accept_mimetype = DEFAULT_ACCEPT_MIMETYPE ,
100
104
max_size_bytes = DEFAULT_MAX_SIZE_BYTES ,
101
105
overwrite_mode = DEFAULT_OVERWRITE_MODE ,
106
+ on_change = None ,
102
107
** kwargs ,
103
108
):
104
109
self ._versioning_enabled = None
@@ -112,6 +117,7 @@ def __init__(
112
117
self .store_key = store_key
113
118
self .accept_mimetype = accept_mimetype
114
119
self .max_size_bytes = max_size_bytes
120
+ self .on_change = on_change
115
121
kwargs ["default" ] = kwargs .pop ("default" , None )
116
122
kwargs ["help_text" ] = kwargs .pop ("help_text" , "GCP cloud storage object" )
117
123
@@ -144,6 +150,7 @@ def check(self, **kwargs):
144
150
* self ._check_get_destination_path (),
145
151
* self ._check_ingress_to (),
146
152
* self ._check_overwrite_mode (),
153
+ * self ._check_on_change (),
147
154
]
148
155
149
156
def deconstruct (self ):
@@ -153,6 +160,7 @@ def deconstruct(self):
153
160
kwargs ["accept_mimetype" ] = self .accept_mimetype
154
161
kwargs ["overwrite_mode" ] = self .overwrite_mode
155
162
kwargs ["get_destination_path" ] = self .get_destination_path
163
+ kwargs ["on_change" ] = self .on_change
156
164
return name , path , args , kwargs
157
165
158
166
def formfield (self , ** kwargs ):
@@ -172,7 +180,11 @@ def formfield(self, **kwargs):
172
180
@property
173
181
def override_blobfield_value (self ):
174
182
"""Shortcut to access the GCP_STORAGE_OVERRIDE_BLOBFIELD_VALUE setting"""
175
- return getattr (settings , "GCP_STORAGE_OVERRIDE_BLOBFIELD_VALUE" , DEFAULT_OVERRIDE_BLOBFIELD_VALUE )
183
+ return getattr (
184
+ settings ,
185
+ "GCP_STORAGE_OVERRIDE_BLOBFIELD_VALUE" ,
186
+ DEFAULT_OVERRIDE_BLOBFIELD_VALUE ,
187
+ )
176
188
177
189
def pre_save (self , model_instance , add ):
178
190
"""Return field's value just before saving."""
@@ -184,7 +196,12 @@ def pre_save(self, model_instance, add):
184
196
# explicitly for the purpose of data migration and manipulation. You should never allow an untrusted
185
197
# client to set paths directly, because knowing the path of a pre-existing object allows you to assume
186
198
# access to it. Tip: You can use django's override_settings context manager to set this temporarily.
187
- if getattr (settings , "GCP_STORAGE_OVERRIDE_BLOBFIELD_VALUE" , DEFAULT_OVERRIDE_BLOBFIELD_VALUE ):
199
+ # Note that you'll have to execute any on_changed
200
+ if getattr (
201
+ settings ,
202
+ "GCP_STORAGE_OVERRIDE_BLOBFIELD_VALUE" ,
203
+ DEFAULT_OVERRIDE_BLOBFIELD_VALUE ,
204
+ ):
188
205
logger .warning (
189
206
"Overriding %s value to %s" ,
190
207
self .__class__ .__name__ ,
@@ -206,8 +223,15 @@ def pre_save(self, model_instance, add):
206
223
elif adding_blank or updating_valid_to_blank :
207
224
new_value = None
208
225
209
- elif adding_valid or updating_blank_to_valid or updating_valid_to_valid :
226
+ # Trigger the on_change callback at the end of the commit when we know the
227
+ # database transaction will work
228
+ def on_commit_blank ():
229
+ if self .on_change is not None :
230
+ self .on_change (new_value , instance = model_instance )
231
+
232
+ transaction .on_commit (on_commit_blank )
210
233
234
+ elif adding_valid or updating_blank_to_valid or updating_valid_to_valid :
211
235
new_value = {}
212
236
new_value ["path" ], allow_overwrite = self .get_destination_path (
213
237
instance = model_instance ,
@@ -228,8 +252,8 @@ def pre_save(self, model_instance, add):
228
252
# capture the dual edge cases of the file not moving correctly, and the database
229
253
# row not saving (eg due to validation errors in other model fields).
230
254
# https://stackoverflow.com/questions/33180727/trigering-post-save-signal-only-after-transaction-has-completed
231
- transaction . on_commit (
232
- lambda : copy_blob (
255
+ def on_commit_valid ():
256
+ copy_blob (
233
257
self .storage .bucket ,
234
258
value ["_tmp_path" ],
235
259
self .storage .bucket ,
@@ -238,9 +262,15 @@ def pre_save(self, model_instance, add):
238
262
overwrite = allow_overwrite ,
239
263
attributes = value .get ("attributes" , None ),
240
264
)
241
- )
265
+ if self .on_change is not None :
266
+ self .on_change (new_value , instance = model_instance )
267
+
268
+ transaction .on_commit (on_commit_valid )
269
+
242
270
logger .info (
243
- "Registered move of %s to %s to happen on transaction commit" , value ["_tmp_path" ], new_value ["path" ]
271
+ "Registered move of %s to %s to happen on transaction commit" ,
272
+ value ["_tmp_path" ],
273
+ new_value ["path" ],
244
274
)
245
275
246
276
else :
@@ -407,6 +437,17 @@ def _check_get_destination_path(self):
407
437
]
408
438
return []
409
439
440
+ def _check_on_change (self ):
441
+ if self .on_change is not None and not callable (self .on_change ):
442
+ return [
443
+ checks .Error (
444
+ f"'on_change' argument in { self .__class__ .__name__ } must be or a callable function, or None" ,
445
+ obj = self ,
446
+ id = "fields.E201" ,
447
+ )
448
+ ]
449
+ return []
450
+
410
451
def _get_allow_overwrite (self , add ):
411
452
"""Return a boolean determining if overwrite should be allowed for this operation"""
412
453
mode_map = {
0 commit comments