def _add_confirmation_options_to_extra_context(self, extra_context): log(f"Adding confirmation to extra_content {self.confirm_add} {self.confirm_change}" ) return { **(extra_context or {}), "confirm_add": self.confirm_add, "confirm_change": self.confirm_change, }
def set(self, key, upload): """ Set file data to cache for 1000s :param key: cache key :param upload: file data """ try: # noqa: WPS229 state = { "name": upload.name, "size": upload.size, "content_type": upload.content_type, "charset": upload.charset, "content": upload.file.read(), } upload.file.seek(0) self.cache.set(key, state, self.timeout) log(f"Setting file cache with {key}") self.cached_keys.append(key) except AttributeError: # pragma: no cover pass # noqa: WPS420
def get(self, key): """ Get the file data from cache using specific cache key :param key: cache key :return: File data """ upload = None state = self.cache.get(key) if state: file = BytesIO() file.write(state["content"]) upload = InMemoryUploadedFile( file=file, field_name="file", name=state["name"], content_type=state["content_type"], size=state["size"], charset=state["charset"], ) upload.file.seek(0) log(f"Getting file cache with {key}") return upload
def changeform_view(self, request, object_id=None, form_url="", extra_context=None): if request.method == "POST": if (not object_id and CONFIRM_ADD in request.POST) or ( object_id and CONFIRM_CHANGE in request.POST): log("confirmation is asked for") self._file_cache.delete_all() cache.delete_many(CACHE_KEYS.values()) return self._change_confirmation_view(request, object_id, form_url, extra_context) elif CONFIRMATION_RECEIVED in request.POST: return self._confirmation_received_view( request, object_id, form_url, extra_context) else: self._file_cache.delete_all() cache.delete_many(CACHE_KEYS.values()) extra_context = self._add_confirmation_options_to_extra_context( extra_context) return super().changeform_view(request, object_id, form_url, extra_context)
def _reconstruct_request_files(): """ Reconstruct the file(s) from the file cache (if any). Returns a dictionary of field name to cached file """ reconstructed_files = {} cached_object = cache.get(CACHE_KEYS["object"]) # Reconstruct the files from cached object if not cached_object: log("Warning: no cached_object") return if type(cached_object) != self.model: # Do not use cache if the model doesn't match this model log(f"Warning: cached_object is not of type {self.model}") return query_dict = request.POST for field in self.model._meta.get_fields(): if not (isinstance(field, FileField) or isinstance(field, ImageField)): continue cached_file = self._file_cache.get( format_cache_key(model=self.model.__name__, field=field.name)) # If a file was uploaded, the field is omitted from the POST since it's in request.FILES if not query_dict.get(field.name): if not cached_file: log(f"Warning: Could not find file cached for field {field.name}" ) else: reconstructed_files[field.name] = cached_file return reconstructed_files
def _change_confirmation_view(self, request, object_id, form_url, extra_context): # This code is taken from super()._changeform_view # https://github.com/django/django/blob/master/django/contrib/admin/options.py#L1575-L1592 to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR)) if to_field and not self.to_field_allowed(request, to_field): raise DisallowedModelAdminToField( "The field %s cannot be referenced." % to_field) model = self.model opts = model._meta if SAVE_AS_NEW in request.POST: object_id = None add = object_id is None if add: if not self.has_add_permission(request): raise PermissionDenied obj = None else: obj = self.get_object(request, unquote(object_id), to_field) if obj is None: return self._get_obj_does_not_exist_redirect( request, opts, object_id) if not self.has_view_or_change_permission(request, obj): raise PermissionDenied fieldsets = self.get_fieldsets(request, obj) ModelForm = self.get_form(request, obj, change=not add, fields=flatten_fieldsets(fieldsets)) form = ModelForm(request.POST, request.FILES, instance=obj) form_validated = form.is_valid() if form_validated: new_object = self.save_form(request, form, change=not add) else: new_object = form.instance formsets, inline_instances = self._create_formsets(request, new_object, change=not add) # End code from super()._changeform_view # form.is_valid() checks both errors and "is_bound" # If form has errors, show the errors on the form instead of showing confirmation page if not form_validated: log("Invalid Form: return early") log(form.errors) # We must ensure that we ask for confirmation when showing errors extra_context = self._add_confirmation_options_to_extra_context( extra_context) return super()._changeform_view(request, object_id, form_url, extra_context) add_or_new = add or SAVE_AS_NEW in request.POST # Get changed data to show on confirmation changed_data = self._get_changed_data(form, model, obj, add_or_new) changed_confirmation_fields = set( self.get_confirmation_fields(request, obj)) & set( changed_data.keys()) if not bool(changed_confirmation_fields): log("No change detected") # No confirmation required for changed fields, continue to save return super()._changeform_view(request, object_id, form_url, extra_context) # Parse the original save action from request save_action = None # No cover: There would not be a case of not request.POST.keys() and form is valid for key in request.POST.keys(): # pragma: no cover if key in SAVE_ACTIONS: save_action = key break cleared_fields = [] if form.is_multipart(): log("Caching files") cache.set(CACHE_KEYS["object"], new_object, CACHE_TIMEOUT) # Save files as tempfiles for field_name in request.FILES: file = request.FILES[field_name] self._file_cache.set( format_cache_key(model=model.__name__, field=field_name), file) # Handle when files are cleared - since the `form` object would not hold that info cleared_fields = self._get_cleared_fields(request) log("Render Change Confirmation") title_action = _("adding") if add_or_new else _("changing") context = { **self.admin_site.each_context(request), "preserved_filters": self.get_preserved_filters(request), "title": f"{_('Confirm')} {title_action} {opts.verbose_name}", "subtitle": str(obj), "object_name": str(obj), "object_id": object_id, "app_label": opts.app_label, "model_name": opts.model_name, "opts": opts, "changed_data": changed_data, "add": add, "save_as_new": SAVE_AS_NEW in request.POST, "submit_name": save_action, "form": form, "cleared_fields": cleared_fields, "formsets": formsets, **(extra_context or {}), } return self.render_change_confirmation(request, context)
def _confirmation_received_view(self, request, object_id, form_url, extra_context): """ When the form is a multipart form, the object and POST are cached This is required because file(s) cannot be programmically uploaded ie. There is no way to set a file on the html form If the form isn't multipart, this function would not be called. If there are no file changes, do nothing to the request and send to Django. If there are files uploaded, save the files from cached object to either: - the object instance if already exists - or save the new object and modify the request from `add` to `change` and pass the request to Django """ log("Confirmation has been received") def _reconstruct_request_files(): """ Reconstruct the file(s) from the file cache (if any). Returns a dictionary of field name to cached file """ reconstructed_files = {} cached_object = cache.get(CACHE_KEYS["object"]) # Reconstruct the files from cached object if not cached_object: log("Warning: no cached_object") return if type(cached_object) != self.model: # Do not use cache if the model doesn't match this model log(f"Warning: cached_object is not of type {self.model}") return query_dict = request.POST for field in self.model._meta.get_fields(): if not (isinstance(field, FileField) or isinstance(field, ImageField)): continue cached_file = self._file_cache.get( format_cache_key(model=self.model.__name__, field=field.name)) # If a file was uploaded, the field is omitted from the POST since it's in request.FILES if not query_dict.get(field.name): if not cached_file: log(f"Warning: Could not find file cached for field {field.name}" ) else: reconstructed_files[field.name] = cached_file return reconstructed_files reconstructed_files = _reconstruct_request_files() if reconstructed_files: log(f"Found reconstructed files for fields: {reconstructed_files.keys()}" ) obj = None # remove the _confirm_add and _confirm_change from post modified_post = request.POST.copy() if CONFIRM_ADD in modified_post: del modified_post[CONFIRM_ADD] # pragma: no cover if CONFIRM_CHANGE in modified_post: del modified_post[CONFIRM_CHANGE] # pragma: no cover if object_id and SAVE_AS_NEW not in request.POST: # Update the obj with the new uploaded files # then pass rest of changes to Django obj = self.model.objects.filter(id=object_id).first() else: # Create the obj and pass the rest as changes to Django # (Since we are not handling the formsets/inlines) # Note that this results in the "Yes, I'm Sure" submission # act as a `change` not an `add` obj = cache.get(CACHE_KEYS["object"]) # No cover: __reconstruct_request_files currently checks for cached obj so obj won't be None if obj: # pragma: no cover for field, file in reconstructed_files.items(): log(f"Setting file field {field} to file {file}") setattr(obj, field, file) obj.save() object_id = str(obj.id) # Update the request path, used in the message to user and redirect # Used in `self.response_change` request.path = get_admin_change_url(obj) if SAVE_AS_NEW in request.POST: # We have already saved the new object # So change action to _continue del modified_post[SAVE_AS_NEW] if self.save_as_continue: modified_post[SAVE_AND_CONTINUE] = True else: modified_post[SAVE] = True if "id" in modified_post: del modified_post["id"] modified_post["id"] = object_id request.POST = modified_post self._file_cache.delete_all() cache.delete_many(CACHE_KEYS.values()) return super()._changeform_view(request, object_id, form_url, extra_context)