I'd like to make sure that nobody can't create an Invitation object with an email that is already in a database either as Invitation.email or as User.email.
To disallow creating Invitation with existing Invitation.email is easy:
class Invitation(..):
email = ...unique=True)
Is it also possible to check for the email in User table? I want to do this on a database or model level instead of checking it in serializer, forms etc..
I was thinking about UniqueConstraint but I don't know how to make the User.objects.filter(email=email).exists() lookup there.
You can override the save() method on the model, and check first in the users table. You should look that is a new model. Something like this I think:
class Invitation(..):
email = ...unique=True)
def save(self, *args, **kwargs):
if self.id is None and User.objects.filter(email=self.email).exists():
raise ValidationError('Email already used.')
else:
super().save(*args, **kwargs)
You can do it in the model.. as below. Or you can do it in the database with a Check Constraint (assuming postgres).. but you still can't avoid adding code to your view, because you'll need to catch the exception and display a message to the user.
class Invitation(models.Model):
def save(self, *args, **kwargs):
if (not self.pk) and User.objects.filter(email=self.email).exists():
raise ValueError('Cannot create invitation for existing user %s.' % self.email)
return super().save(*args, **kwargs)
PS: Some may ask why it is that I am passing *args and **kwargs to the superclass, or returning the return value.. when save has no return value. The reason for this is that I never assume that the arguments or return value for a method I am overriding won't change in the future. Passing them all through if you have no reason to intercept them, is just a good practice.
How about overriding the save method?
class Invitation(...):
...
def save(self, *args, **kwargs):
# check if an invitation email on the user table:
if User.objects.get(id=<the-id>).email:
# raise integrity error:
...
# otherwise save as normal:
else:
super().save(*args, **kwargs)
Related
I am creating a django DB model and I want one the fields to be readonly. When creating a new object I want to set it, but later if someone tries to update the object, it should raise an error. How do I achieve that?
I tried the following but I was still able to update the objects.
from django.db import models as django_db_models
class BalanceHoldAmounts(django_db_models.Model):
read_only_field = django_db_models.DecimalField(editable=False)
Thank you
You can override it in the "save" method of the model and raise a Validation Error if someone tries to update that field.
def save(self, *args, **kwargs):
if self.pk:
previous_value = BalanceHoldAmounts.objects.get(pk=self.pk)
if previous_value.read_only_field != self.read_only_field:
raise ValidationError("The read_only_field can not be changed")
super().save(*args, **kwargs)
How do we enforce Django Admin to correctly call .update() instead of .save() to avoid triggering checks meant for object creation?
This is the models.py:
class BinaryChoice():
# field definitions
...
def save(self, *args, **kwargs):
# check if binary
if self.question.qtype == 2:
if self.question.choices.count() < 2:
super(BinaryChoice, self).save(*args, **kwargs)
else:
raise Exception("Binary question type can contain at most two choices.")
else:
super(BinaryChoice, self).save(*args, **kwargs)
This passes the test, no surprises:
class SurveyTest(TestCase):
def test_binary_choice_create(self):
q1 = Question.objects.create(survey=survey, title='Have you got any internship experience?', qtype=Question.BINARY)
BinaryChoice.objects.create(question=q1, choice="Yes")
BinaryChoice.objects.create(question=q1, choice="No")
with self.assertRaises(Exception):
BinaryChoice.objects.create(question=q1, choice="Unsure / Rather not say")
The .save() correctly checks that there isn't already 2 binary choices related to the same Question. However, in Django Admin, when using the interface to update the value (anything arbitrary, for example changing the value from "Yes" to "Sure") and saving it, one would expect the .update() method to be called.
It turns out, according to Django docs and also a relevant thread here, the .save() method is called instead. So now our update operation would fail when there's already 2 BinaryChoice, even if you intend to update a value in-place using the Django Admin's default interface.
For completeness sake, this is admin.py:
#admin.register(BinaryChoice)
class BinaryChoiceAdmin(admin.ModelAdmin):
pass
Instead of trying to patch the ModelAdmin why don't you simply fix your save method? Simply check if the object already has a pk or not before saving:
class BinaryChoice():
# field definitions
...
def save(self, *args, **kwargs):
# check if binary
# Here ↓
if not self.pk and self.question.qtype == 2:
if self.question.choices.count() < 2:
super(BinaryChoice, self).save(*args, **kwargs)
else:
raise Exception("Binary question type can contain at most two choices.")
else:
super(BinaryChoice, self).save(*args, **kwargs)
I`m new to django and i was doing a test for my knowledge.
Found a lot of duplicates in here and web but nothing useful
I'm trying to make a ForeignKey field which gets filled due to the other fields that user fills, and make it unchangeable for the user.
I thought that I should use overriding save() method but couldn't figure that at all.
How should I do that auto-fill and read-only thing?
Your approach is right. Override the save method and if self.pk is not None raise an exception if your field has changed. You can use django model utils to easily track changes in your model: https://django-model-utils.readthedocs.io/en/latest/utilities.html#field-tracker
Principle:
class MyModel(models.Model):
#....
some_field = models.Foreignkey(...)
tracker = FieldTracker()
def save(*args, **kwargs):
if self.pk is None:
# new object is being created
self.some_field = SomeForeignKeyObject
else:
if self.tracker.has_changed("some_field"):
raise Exception("Change is not allowed")
super().save(*args, **kwargs)
Recently I've been trying to do something with this.
Think of the family as a facebook group.
class Family(models.Model):
name = models.CharField(max_length=50)
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='owned_families')
users = models.ManyToManyField(User, related_name='families', blank=True)
let's assume we have this family object called fm, for illustration purpose.
My problem is, The owner is one of the users right? I mean, When someone creates a family, He's now the owner right? he owns it but he's still a user listed in it's users list.
Now, when I create a new family fm , I want to add the fm.owner to fm.users.
Let's talk about what I've tried.
post_save signal doesn't work with m2m. X
m2m_changed happens when the field is changed, not created. X
Overriding save method, lemme illustrate what I tried to acheive. ?
def save(self, *args, **kwargs):
old = self.pk
super(Family, self).save(*args, **kwargs)
if old is None:
print('This actually shows up')
self.users.add(self.owner)
Basically, this saves the pk each time, First time a family is created, Before calling super..... it has no .pk so I'm counting on this to check if it had no pk (On creation).
The problem is self.users.add(self.owner) doesn't work.
I've tried to clone the object as whole and keep track of it like
def save(self, *args, **kwargs):
old = self
super(Family, self).save(*args, **kwargs)
if old is None:
print("This actually doesn't show up")
self.users.add(self.owner)
This actually is terrible, It takes a refernce to self and when calling super...., The selfand it's reference old gets mutated, I just wanted to show this as this question itself might solve someone's problem.
So I solved this by.
import copy
def save(self, *args, **kwargs):
old = copy.deepcopy(self)
super(Family, self).save(*args, **kwargs)
if old is None:
print('This actually shows up')
self.users.add(self.owner)
but self.users.add(self.owner) still doesn't work.
What am I missing?
The problem is probably that in the django admin, the instance is saved first, and only after that the inline formsets and m2m-fields are saved. If the owner is not in there, it will be removed.
You can override some functionality in the admin to remedy this:
class FamilyAdmin(ModelAdmin):
def save_related(self, request, form, formsets, change):
super(FamilyAdmin, self).save_related(request, form, formsets, change)
form.instance.users.add(form.instance.owner)
Furthermore, you can try (note that there are other ways to remove the owner that are not picked up by any signal or other hook) to prevent code from removing the owner:
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
#receiver(m2m_changed, sender=Family.users.through)
def famliy_users_changed(sender, **kwargs):
family = kwargs['instance']
pk_set = kwargs['pk_set']
action = kwargs['action']
if action == "pre_add":
pk_set.add(family.owner_id)
if action == "pre_remove":
pk_set.remove(family.owner_id)
if action == "post_clear":
family.users.add(family.owner)
But generally speaking, you are jumping through those hoops because you are denormalizing your data (putting the owner in users makes that information redundant, forcing you to keep your data correct). Since you always know the owner is one of the users, why not wrap that in a method
class Family(...):
# ...
def members(self):
return User.objects.filter(Q(pk__in=self.users.all()|Q(pk=self.owner_id)))
and access family members through that method?
I am trying to add logging to my Django app using EventLog. I followed an example online but not sure how to pass in the user that makes the changes. The example shows it as user=self.user. Obviously this wouldn't work in my case as it doesn't refer to anything in my model
models.py
class Client(models.Model):
name = models.CharField(max_length=50)
....
def save(self, *args, **kwargs):
# Initial Save
if not self.pk:
log(user=self.user, action='ADD_CLIENT',
extra={'id': self.id})
else:
log(user=self.user, action='UPDATED_CLIENT',
extra={'id': self.id})
super(Client, self).save(*args, **kwargs)
The save method will only know what has been passed into it, this will normally not include the request which is where you would get the current user (request.user).
You should instead add logging in the view which is calling the save method.
user = request.user