How to delete all nested objects referenced to an object? - python

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.'})

Related

how to use try accept in a better way while making an object of model class to get data?

looking for help to handle the exception in a better way, I am new to python and django so if any one can suggest me that what can i write in place of pass, as i dont have any code to write there can i return Response(status=status.HTTP_200_OK) where pass is written or is there any thing which is better than this ?
if(computedsignature==signature):
try:
order=Orders.objects.get(id=id)
except (Orders.DoesNotExist):
pass
payment_collection_webhook_url=order.payment_collection_webhook_url
application_id = order.applications
try:
application = Applications.objects.get(id=application_id)
except (Applications.DoesNotExist):
pass
if(transaction_status=='SUCCESS'):
try:
payment= Payments.objects.get(orders=order,direction=direction)
except (Payments.DoesNotExist):
payment.save()
It does not make much sense to wrap the result of database queries in a try-except. If it Order can not be found or the Payment, etc. then clearly something is wrong with the request, or with your view.
Usually it is better not to silence exceptions, since then it looks like everything works fine. But if no Order can be found, likely something is broken, or someone is attempting for example to hack the system and get an order for free.
One can work with the get_object_or_404(…) function [Django-doc] to return HTTP 404 in case the query is missing, thus:
from django.http import Http404
from django.shortcuts import get_object_or_404
if computedsignature == signature:
order = get_object_or_404(Orders, id=id)
payment_collection_webhook_url = order.payment_collection_webhook_url
application = get_object_or_404(Applications, pk=order.applications)
if transaction_status == 'SUCCESS':
payment = get_object_or_404(Payments, orders=order,direction=direction)
else:
raise Http404('the payment was not successful')
You have to treat your exceptions in some way, otherwise your try-except clause has no sense.
Here you have two common examples:
# If the object does not exists a HTTP 404 is returned.
try:
your_model_instance = YourModel.objects.get(id=your_model_id)
except YourModel.DoesNotExist:
raise Http404()
# Equivalent simpler code.
your_model_instance = get_object_or_404(YourModel, id=your_model_id)
# If the object does not exists a new instance is created.
try:
obj = YourModel.objects.get(id=your_model_id)
except Person.DoesNotExist:
obj = YourModel(code='0001', name='Example')
obj.save()
# Equivalent simpler code.
your_model_instance = YourModel.objects.get_or_create(id=your_model_id, defaults={'code': '0001', 'name': 'Example'})
Even though, you can do whatever you want in your except clause. But remember that pass should only be placed in an except if you don't care about that exception and that is not your case.
More info about get_object_or_404 here and more info about get_or_create here.

How to override delete logic on Flask Admin?

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.

Better way to set default value in django model field

I am implementing a field in django model which calls the following function for default value
def get_default_value():
a = MyModel.objects.aggregate(max_id=Max('id'))
return get_unique_value_based_on_num(a['max_id'] or 0)
class MyModel:
default_value_field = CharField(default=get_default_value)
Although I may be wrong on this, I fear this implementation may result in race condition.
Is there a better way to do this ? May be use F object or something else ?
To avoid race conditions, it is best letting the database handle the integrity of your table, which is one of the things databases are made for.
To do so, catch any IntegrityError raised by saving your model instance and try again with a different value when it fails.
from django.db import IntegrityError, models, transaction
def get_default_value():
a = MyModel.objects.aggregate(max_id=Max('id'))
return get_unique_value_based_on_num(a['max_id'] or 0)
class MyModel(models.Model):
# Have unicity enforced at database level with unique=True.
default_value_field = models.CharField(max_length=200, unique=True)
def save(self):
if not self.default_value_field:
max_tries = 100 # Choose a sensible value!
for i in range(max_tries):
try:
self.default_value_field = get_default_value()
# Atomic block to rollback transaction in case of IntegrityError.
with transaction.atomic():
super(MyModel, self).save()
break
except IntegrityError:
# default_value_field is not unique, try again with a new value.
continue
else:
# Max tries reached, raise.
raise IntegrityError('Could not save model because etc...')
else:
super(MyModel, self).save(*args, **kwargs)
Seems like django has select_for_update for queryset API. It may be the solution to my problem.
https://docs.djangoproject.com/en/dev/ref/models/querysets/#select-for-update

Django IntegrityError, get error number or code

I want to check for IntegrityError(1048, "Column 'ean' cannot be null").
What is the proper way to do this? I feel like I'm not using the best method.
Product class
class Product(models.Model):
ean = models.BigIntegerField()
name = models.CharField()
Currently I'm doing this crazy trick.
newProduct = Product(ean=None, name='Foo')
try:
newProduct.save()
except IntegrityError, e:
error = e
code = error.__getslice__(0,1)
code = error[0]
# handle error 1048
I would love to see a proper example of handling a specific IntegrityError in Python / Django.
I think the best solution is not to handle the IntegrityError but to validate the model instance before saving it:
# Run validation
try:
newProduct.full_clean()
except ValidationError as error:
error.error_dict # A dictionary mapping field names to lists of ValidationErrors.
error.message_dict # A dictionary mapping field names to lists of error messages.
# In your case you can check
for e in error.error_dict.get('ean'):
if e.code == 'blank':
# ean was empty
IntegrityError exception handling is little different ,So to fetch number.
e.args[0]
For more information : https://www.programcreek.com/python/example/5321/MySQLdb.IntegrityError
Field ean is an required field,you cannot pass None value into it.To fix this issue add
ean = models.BigIntegerField(null=True,blank=True)
Then it wont show integrity error.Becuase right now the field is expecting some data not null value.
Otherwise
newProduct = Product(ean=256, name='Foo')
pass an integer value.This worked for me.
You can also right some form validation to prevent this issue.
example:
def clean_ean(self):
data = self.cleaned_data['ean']
if "fred#example.com" not in data:
raise forms.ValidationError("message")
# Always return the cleaned data, whether you have changed it or
# not.
return data
Refer:https://docs.djangoproject.com/en/1.8/ref/forms/validation/
Django has some optional parameters for checking content of fields such as null and blank for checking that field is not empty, so has some validators that you can use for checking content of fields for example use django MinLengthValidator for checking length of ean field content:
from django.core.validators import MinLengthValidator
class Product(models.Model):
ean = models.BigIntegerField(null=False, validators=MinLengthValidator(1, message='The "ean" field min length is one char at least.'))
name = models.CharField()
By using MinLengthValidator you can return custom message if ean field is empty.
I made a method to validate the product object before saving it. This way I'm sure that I can save the product without running into errors. Thanks for the tips!
For more information see that Django documentation on validating objects.
class Product(model.Models):
def saveProduct(self, product):
if self.validateProduct(product):
product.save()
def validateProduct(self, product):
try:
product.full_clean()
except ValidationError:
return False
return True
You can try
ean = models.BigIntegerField(requried=True)
...
newP = Product(name='Foo')
This result may be what you want.

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