Django model calls an API before save - python

I have a Django model that needs to call an external API just before saving. The API call is irreversible so I want to call it only before the DB save happens.
My code looks something like this:
class Model1(models.Model):
some_string = models.CharField(max_length=100)
class Model2(models.Model):
model1 = models.OneToOneField(Model1, on_delete=models.CASCADE)
num1 = models.IntegerField()
num2 = models.IntegerField()
api_output = models.models.JSONField()
def save(self, *args, **kwargs):
self.api_output = API.call_method(self.num1, self.num2, self.model1.some_string)
super().save(*args, **kwargs)
Model2 objects can be created from Django Admin but also from the code using Model2.objects.create().
I have 2 questions:
When creating an instance of Model2 from Django Admin - if the API call fails (throws an exception) I'd like Django Admin to show a human-readable error instead of the 5xx error page. One way to do that is to have a clean() method but I don't want to call the API before the object is actually saved. Also, when using Model2.objects.create() the clean() method is not called. Is there a way to call the API inside save() and still have Django Admin print a nice error if the call fails?
I noticed that during save(), if the OneToOneField constraint is violated (for example trying to create 2 instances of Model2 with the same instance of Model1), the validation happens after the API call which means the API is being called although the object is not valid. Is there a way to perform all validations before my code runs (which call the API)?

You should use Django Signals for that kind of business logic, if you want to do something before saving a model check the pre_save signal.
If you want to use Django Admin, you have to work in your admin.py inside your app and override the save_model method of ModelAdmin.

Related

Django form for adding emails connected to models

This is a bit hard to explain but I'll try my best.
I am developing a website where I want a feature where the admins can add an email notification connected to a certain model. The admin should be able to
Choose a model on the website,
What event it should trigger on (create, update or delete),
The email body and text.
For example, if the admin adds an email to a model foo on a create event, the specified email should be sent to some recipient whenever a new foo is added to the database.
I am attempting to implement a new model that contains some reference to the models, using post_save() to send emails, but it's turning out more complex than I thought. I have searched far and wide to find some addon that does this, but I haven't found any.
Does anyone have any tips on specific functionalities that can help with this, or if there's a good addon that I can start with?
Create a new model (called "SendEmail" for example) that contain the relations and the data of the emails is a good idea. May be you can override save and delete methods on the other models. In your views, before save or delete an object, check in your new model "SendEmail" if it is necessary to send and email and pass this object to the save/delete method.
In your views, after check, if you have to send the email:
foo.save(sendemail=sendemail) #where sendemail is a SendEmail object.
if not:
foo.save()
In your existing models:
Class Foo(models.Model):
----
----
def save(self, *args, **kwargs, sendemail=None):
if sendemail:
**the code to send the email**
super().save(*args, **kwargs) # Call the "real" save() method.
EDIT:
To not override all de save method for all your models you can create a new model class with the save method and change the class of your actual models:
Class SendEmailModel(models.Model):
def save(self, *args, **kwargs, sendemail=None):
if sendemail:
**the code to send the email**
super().save(*args, **kwargs) # Call the "real" save() method.
Class Foo(SendEmailModel):
----
----

Is there a way to declare a mock model in Django unit tests?

Title says it all. I'll illustrate the question by showing what I'm trying to do.
I have extended Django's ModelForm class to create a ResourceForm, which has some functionality built into its clean() method for working with Resources, the details of which are unimportant. The ResourceForm is basically a library class, and there are no models in the app where the ResourceForm class is defined, so I can't just use an existing model from the app (e.g., mock.Mock(spec=SomeModel) is not an option).
I am trying to unit test ResourceForm, but I can't figure out the right way to mock a Django Model, which is required since ResourceForm inherits from ModelForm. This is one of several efforts I have tried (not using mock in this case, but it serves to illustrate what is being attempted):
class ResourceFormTestCase(TestCase):
class SampleModel(Model):
sample_field = CharField()
class SampleResourceForm(ResourceForm):
class Meta():
model = SampleModel
fields = ['sample_field']
def test_unsupported_field_raise_validation_error(self):
print('Test validation error is raised when unsupported field is provided')
form_data = {'sample_field': 'FooBar', 'unsupported_field': 'Baz'}
form = self.SampleResourceForm(data=form_data)
But that raises:
RuntimeError: Model class customer.tests.tests_lib_restless_ext.SampleModel doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS.
I'm open to suggestions if I'm way off-base in how I'm trying to test this.
The simplest thing that might work is to use the user model that comes with Django.
If that's not acceptable, I have successfully patched enough of the Django plumbing to make it shut up and run unit tests without a database connection. Look in the django_mock_queries project to see if any of that is helpful.

Validate model instance from python using Django admin validators

I'm working on the importing script that saves data from CSV to Django database. Saving process looks like this:
instance = ModelName(**kwargs)
instance.save()
# No errors reported
But when I try to edit and save some items using admin panel it shows me a message that some of the field values (like URL fields and custom validators) is not valid.
Question: Is there any way to validate model instance from the python code using Django admin validators?
The issue is save() does not validate by default.
To address this, you can call the model's full_clean method to validate before calling save.
So, under the hood,
This method calls Model.clean_fields(), Model.clean(), and
Model.validate_unique() (if validate_unique is True), in that order
and raises a ValidationError that has a message_dict attribute
containing errors from all three stages.
The code would look something like this:
instance = ModelName(**kwargs)
instance.full_clean() #Does validation here
instance.save()

Is there a way in Django to get the currently authenticated user directly from a model?

Like if I have a Message model like
class Message(models.Model):
from = models.ForeignKey(User)
to = models.ManyToManyField(User)
def get_authenticated_user_inbox_messages(self):
return Message.objects.filter(to=authenticated_user).all()
authenticated_user being a way to get the currently authenticated user.
In the view, I can get the authenticated user with request.user
But, if I don't want or can't by design to pass this as a parameter, is there a way to get it directly from inside the model?
Any ideas? Thank you very much!
No, this is not possible. A Model object exists independently from a web request. For example, you can define a management command that uses model objects from the command-line, where there is no request.
What I've seen as a good practice is to define a static method on your Model object that takes a User object as input, like this:
class Message(models.Model):
from = models.ForeignKey(User)
to = models.ManyToManyField(User)
#staticmethod
def get_messages_to(user):
return Message.objects.filter(to=user)
#staticmethod
def get_messages_from(user):
return Message.objects.filter(from=user)
You could also define a custom manager for the model and define these convenience methods there instead.
EDIT: Okay, technically it is possible, but I wouldn't recommend doing it. For a method, see Global Django Requests.
When you connect the set up a foreign key from Messages to Users, django automatically creates a related field on the User model instances. (see https://docs.djangoproject.com/en/dev/topics/db/queries/#backwards-related-objects)
So, if you have a user object, you should be able to access user_object.messages.all(). This feels like a more django-appropriate way to do what your get_authenticated_user_inbox_messages() method is doing.

Setting the value of a model field based on user authentication in Django

I'm trying to selectively process a field in my Django/Python application based on whether a user is logged in or not. Basically, I have a model similar to the following:
class Resource(models.Model):
uploaded = models.DateTimeField()
name = models.CharField(max_length=200)
description = models.CharField(max_length=500, blank=True)
file = models.CharField(max_length=200)
What I want to do is for the file attribute to be set to one value if the user happens to be logged in (and has access to this resource based on a test against some permissions backend), and another value if the user is not logged in. So, when any client code tries to access Resource.file, it will get something like the following if the user is not logged in 'http://mysite.com/dummy_resource_for_people_without_access'. However, if the user is logged in and passes some tests for permissions, then the value of resource.file will actually be the true url of that resource (including any security keys etc. to access that resource).
From what I've read, it seems that you can only take account of the currently logged in user by passing that through the request context from a view function to the model. However, in the above use case I am trying to control the access more closely in the model without needing the client code to call a special function.
Your best bet is to create a function used to access the file attribute, and check there. In general, it would possible to turn the attribute into a descriptor which does it implicitly, but Django's metaclass magic would impede that.
In general however, Django is designed to handle authentication at the view-level (and does it very cleanly). If you need database layer authentication, consider a different setup, such as CouchDB.
Just in case anyone's interested, I solved the above issue by actually creating a custom model field in django that could then have a method that takes a user to generate a URI. So, in the database, I store a key to the resource as above in the file column. However, now the file column is some custom field:
class CustomFileField(models.CharField):
def to_python(self, value):
...
return CustomFileResource(value)
class CustomFileResource:
def __init__(self, *args, **kwargs):
....
def uri(usr):
#this method then gets the uri selectively based on the user .
The pattern above is nice because I can wrap the db field and then create a specific method for getting the uri based on who is trying to access it.

Categories

Resources