How to override delete logic on Flask Admin? - python

I have been searching on Google and StackOverflow about this. Basically, I want to try and override the delete function on Flask-Admin to not actually delete a record, but instead update a row of the object called 'deleted_by' and 'deleted_on'.
I have found a few questions on StackOverflow that explain how to change the logic on the save button by using on_model_change, but no one specific about the delete model logic. I have also not found any information regarding this on the documentation. Could anyone show me how should I handle this issue?
Thanks in advance!

Override method delete_model in your view. Here is the default behaviour if you are using Sqlalchemy views, note the call to self.session.delete(model) in the try ... except block.
def delete_model(self, model):
"""
Delete model.
:param model:
Model to delete
"""
try:
self.on_model_delete(model)
self.session.flush()
self.session.delete(model)
self.session.commit()
except Exception as ex:
if not self.handle_view_exception(ex):
flash(gettext('Failed to delete record. %(error)s', error=str(ex)), 'error')
log.exception('Failed to delete record.')
self.session.rollback()
return False
else:
self.after_model_delete(model)
return True
You would need something like the following in your view:
class MyModelView(ModelView):
def delete_model(self, model):
"""
Delete model.
:param model:
Model to delete
"""
try:
self.on_model_delete(model)
# Add your custom logic here and don't forget to commit any changes e.g.
# self.session.commit()
except Exception as ex:
if not self.handle_view_exception(ex):
flash(gettext('Failed to delete record. %(error)s', error=str(ex)), 'error')
log.exception('Failed to delete record.')
self.session.rollback()
return False
else:
self.after_model_delete(model)
return True
Also, you might not want to bother with the self.on_model_delete(model) and self.after_model_delete(model) calls because by default they do nothing.

Related

Flask Admin - Access old values using on_model_change()

My goal is to perform some additional action when a user changes a value of a existing record.
I found on_model_change() in the docs and wrote the following code:
def on_model_change(self, form, model, is_created):
# get old and new value
old_name = model.name
new_name = form.name
if new_name != old_name:
# if the value is changed perform some other action
rename_files(new_name)
My expectation was that the model parameter would represent the record before the new values from the form was applied. It did not. Instead i found that model always had the same values as form, meaning that the if statement never was fulfilled.
Later i tried this:
class MyView(ModelView):
# excluding the name field from the form
form_excluded_columns = ('name')
form_extra_fields = {
# and adding a set_name field instead
'set_name':StringField('Name')
}
...
def on_model_change(self, form, model, is_created):
# getting the new value from set_name instead of name
new_name = form.set_name
...
Although this solved my goal, it also caused a problem:
The set_name field would not be prefilled with the existing name, forcing the user to type the name even when not intending to change it
I also tried doing db.rollback() at the start of on_model_change() which would undo all changes done by flask-admin, and make model represent the old data. This was rather hacky and lead my to reimplement alot of flask admin code myself, which got messy.
What is the best way to solve this problem?
HOW I SOLVED IT
I used on_form_prefill to prefill the new name field instead of #pjcunningham 's answer.
# fill values from model
def on_form_prefill(self, form, id):
# get track model
track = Tracks.query.filter_by(id=id).first()
# fill new values
form.set_name.data = track.name
Override method update_model in your view. Here is the default behaviour if you are using SqlAlchemy views, I have added some notes to explain the model's state.
def update_model(self, form, model):
"""
Update model from form.
:param form:
Form instance
:param model:
Model instance
"""
try:
# at this point model variable has the unmodified values
form.populate_obj(model)
# at this point model variable has the form values
# your on_model_change is called
self._on_model_change(form, model, False)
# model is now being committed
self.session.commit()
except Exception as ex:
if not self.handle_view_exception(ex):
flash(gettext('Failed to update record. %(error)s', error=str(ex)), 'error')
log.exception('Failed to update record.')
self.session.rollback()
return False
else:
# model is now committed to the database
self.after_model_change(form, model, False)
return True
You'll want something like the following, it's up to you where place the check, I've put it after the model has been committed:
def update_model(self, form, model):
"""
Update model from form.
:param form:
Form instance
:param model:
Model instance
"""
try:
old_name = model.name
new_name = form.name.data
# continue processing the form
form.populate_obj(model)
self._on_model_change(form, model, False)
self.session.commit()
except Exception as ex:
if not self.handle_view_exception(ex):
flash(gettext('Failed to update record. %(error)s', error=str(ex)), 'error')
log.exception('Failed to update record.')
self.session.rollback()
return False
else:
# the model got committed now run our check:
if new_name != old_name:
# if the value is changed perform some other action
rename_files(new_name)
self.after_model_change(form, model, False)
return True
There are similar methods you can override for create_model and delete_model.

How to delete all nested objects referenced to an object?

I have a model which is set as foreign key in several models. Right now deleting any object from the model throws ProtectedError if it is referenced in any of those model. I want to let user delete the object with all protected objects in a single operation.
I can delete the first layer of protected objects by simply calling
....
except ProtectedError as e
e.protected_objects.delete()
....
But when the protected_objects have their own protected objects, the operation fails and throws another second layer ProtectedError. What I want to achieve is, deleting all the protected objects undiscriminating in which layer it exists. I am aware that it can be a dangerous operation to perform. But can I achieve this without a complex solution. Thanks in advance.
Source Code, where I am trying to perform the ajax operation:
try:
obj_list = model.objects.filter(pk__in=pk_list)
log_deletion(request, obj_list, message='Record Deleted')
obj_list.delete()
return JsonResponse({'success': True, 'status_message': '{0} record(s) has been deleted successfully.'.format(len(pk_list))})
except ProtectedError as e:
e.protected_objects.delete()
return JsonResponse({'success': False, 'status_message': 'This operation cannot be executed. One or more objects are in use.'})
It seems like you might not want to use on_delete=models.PROTECT on the definition of the foreign keys. Have you considered changing the on delete to use CASCADE instead? If you use cascade, you won't need to iterate over the dependencies to delete them first.
Rather than:
class OtherModel(models.Model):
link = models.ForeignKey("Link", on_delete=models.PROTECT)
You could define the model like:
class OtherModel(models.Model):
link = models.ForeignKey("Link", on_delete=models.CASCADE)
When deleting models from the admin that use CASCADE an intermediate page will be shown that lists all the dependent objects that will also be deleted.
In general, you could use a loop:
...
except ProtectedError as e:
obj = e.protected_objects
while True:
try:
obj.delete()
except ProtectedError as e:
obj = e.protected_objects
else:
break
...
To log which layer the errors happen in, you can add a counter:
from itertools import count
obj_list = model.objects.filter(pk__in=pk_list)
for layer in count():
try:
log_deletion(request, obj_list, message='Record Deleted in layer {}'.format(layer))
obj_list.delete()
except ProtectedError as e:
obj_list = e.protected_objects
else:
if layer == 0:
return JsonResponse({'success': True, 'status_message': '{0} record(s) has been deleted successfully.'.format(len(pk_list))})
else:
return JsonResponse({'success': False, 'status_message': 'This operation cannot be executed. One or more objects are in use.'})

Check if record exists in Django Rest Framework API LIST/DATABASE

I want to create a viewset/apiview with a path like this: list/<slug:entry>/ that once I provide the entry it will check if that entry exists in the database.
*Note: on list/ I have a path to a ViewSet. I wonder if I could change the id with the specific field that I want to check, so I could see if the entry exists or not, but I want to keep the id as it is, so
I tried:
class CheckCouponAPIView(APIView):
def get(self, request, format=None):
try:
Coupon.objects.get(coupon=self.kwargs.get('coupon'))
except Coupon.DoesNotExist:
return Response(data={'message': False})
else:
return Response(data={'message': True})
But I got an error: get() got an unexpected keyword argument 'coupon'.
Here's the path: path('check/<slug:coupon>/', CheckCouponAPIView.as_view()),
Is there any good practice that I could apply in my situation?
What about trying something like this,
class CheckCouponAPIView(viewsets.ModelViewSet):
# other fields
lookup_field = 'slug'
From the official DRF Doc,
lookup_field - The model field that should be used to for performing
object lookup of individual model instances. Defaults to pk

save() prohibited to prevent data loss due to unsaved related object

I need to pass a primary key from a newly created ModelForm to another form field in the same view but I get an error. Any suggestions to make this work?
It looks like in the past, this would be the answer:
def contact_create(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse(contact_details, args=(form.pk,)))
else:
form = ContactForm()
From the documentation, this is what is happening in the newer Django version > 1.8.3
p3 = Place(name='Demon Dogs', address='944 W. Fullerton')
Restaurant.objects.create(place=p3, serves_hot_dogs=True, serves_pizza=False)
Traceback (most recent call last):
...
ValueError: save() prohibited to prevent data loss due to unsaved related object 'place'.
This is how I am getting my pk from the view:
my_id = ""
if form.is_valid():
# deal with form first to get id
model_instance = form.save(commit=False)
model_instance.pub_date= timezone.now()
model_instance.user= current_user.id
model_instance.save()
my_id = model_instance.pk
if hourformset.is_valid():
hourformset.save(commit=False)
for product in hourformset:
if product.is_valid():
product.save(commit=False)
product.company = my_id
product.save()
else:
print(" modelform not saved")
return HttpResponseRedirect('/bizprofile/success')
it is simple:
p3 = Place(name='Demon Dogs', address='944 W. Fullerton')
p3.save() # <--- you need to save the instance first, and then assign
Restaurant.objects.create(
place=p3, serves_hot_dogs=True, serves_pizza=False
)
This was introduced in Django 1.8. Previously you could assign not saved instance to One-To-One relation and in case of fail it was silently skipped. Starting from Django 1.8 you will get error message in this case.
Check a documentation of Django 1.7 -> 1.8 upgrade.
It says:
Assigning unsaved objects to a ForeignKey, GenericForeignKey, and
OneToOneField now raises a ValueError.
If you are interested in more details, you can check save method in django.db.models.base: Some part of it:
for field in self._meta.concrete_fields:
if field.is_relation:
# If the related field isn't cached, then an instance hasn't
# been assigned and there's no need to worry about this check.
try:
getattr(self, field.get_cache_name())
except AttributeError:
continue
obj = getattr(self, field.name, None)
# A pk may have been assigned manually to a model instance not
# saved to the database (or auto-generated in a case like
# UUIDField), but we allow the save to proceed and rely on the
# database to raise an IntegrityError if applicable. If
# constraints aren't supported by the database, there's the
# unavoidable risk of data corruption.
if obj and obj.pk is None:
raise ValueError(
"save() prohibited to prevent data loss due to "
"unsaved related object '%s'." % field.name
)
Last 5 rows are where this error is raised. basically your related obj which is not saved will have obj.pk == None and ValueError will be raised.
Answered - The problem arose from django not saving empty or unchanged forms. This led to null fields on those unsaved forms. Problem was fixed by allowing null fields on foreign keys, as a matter of fact -- all fields. That way, empty or unchanged forms did not return any errors on save.
FYI: Refer to #wolendranh answer.
I just removed my model.save() and the error went away.
This only works if you are saving it when there are no changes.
Otherwise you should save it one time, if you changed something in the queryset.
an example:
views.py
queryset = myModel.objects.get(name="someName", value=4)
queryset.value = 5
# here you need to save it, if you want to keep the changes.
queryset.save()
...
# If you save it again, without any changes, for me I got the error save() prohibited to prevent data loss due to unsaved related object
# don't save it again, unless you have changes.
# queryset.save()

Confuse for using get queryset and using pass to ignore an exception and proceed in my django view

I wrote a view to update my draft object , before updating my draft I need to see if any draft exists for package(draft.package) in db or not .
If any draft available, i need to update that draft's fields.
I am using get queryset to look into db to check draft availability.
I want to know that using get queryset here is good way or not and using pass into except.
My View
def save_draft(draft, document_list):
"""
"""
try:
draft = Draft.objects.get(package=draft.package)
except Draft.DoesNotExist as exc:
pass
except Draft.MultipleObjectsReturned as exc:
raise CustomException
else:
draft.draft_document_list.filter().delete()
draft.draft_document_list.add(*document_list)
draft.save()
Extra Information :
models.py
class Package(models.Model):
name = models.CharField(max_length=100)
# -- fields
class Document(models.Model):
# -- fields
Class Draft(models.Model):
# --- fields
package = models.ForeignKey(Package)
draft_document_list = models.ManyToManyField(Document)
My Algorithm :
# first check to see if draft exists for package
# if exists
# overwrite draft_document_list with existed draft and save
# if none exists
# update passed draft object with draft_document_list
Input variables
save_draft(draft, document_list)
draft --> latest draft object
document_list --> list of documents mapped with Draft as M2M.
Yes, for for you models and method signature you use get right. To simplify things you can get rid of delete()/add() methods by direct assign document_list to M2M relation.
def save_draft(draft, document_list):
try:
draft = Draft.objects.get(package=draft.package)
except Draft.DoesNotExist:
pass
except Draft.MultipleObjectsReturned:
raise CustomException
draft.draft_document_list = document_list
draft.save()
EDIT: If there can be only one draft per package then why you use ForeignKey(Package)? With OneToOne relation your code will be much simpler:
def save_draft(draft, document_list):
draft.draft_document_list = document_list
draft.save()

Categories

Resources