Adding an item to many to many after creation in django - python

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?

Related

django - disable Invitation object creation if User object with the email exists

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)

How to rewrite the Django model save method?

How to rewrite the Django model save method?
class Message(models.Model):
"""
message
"""
message_num = models.CharField(default=getMessageNum, max_length=16)
title = models.CharField(max_length=64)
content = models.CharField(max_length=1024)
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
# I want send email there
pass
I mean, in the Django model, if I create instance success, I want to call a function, such as send a email in the function.
I find in the Django model have a save method. I am not sure whether should write other code, because there are so many params.
I mean whether I only should care about my send email logic?
When you override the save method, you still have to make sure that the it actually saves the instance. You can do that by simply calling the parent class' save via super:
class Message(models.Model):
# ...
def save(self, *args, **kwargs):
# this will take care of the saving
super(Message, self).save(*args, **kwargs)
# do email stuff
# better handle ecxeptions well or the saving might be rolled back
You can also connect the mail sending to the post_save (or pre_save, depending on your logic) signal. Whether you want to separate one orm the other in that way depends on how closely the two actions are linked and a bit on your taste.
Overriding save gives you the option to intervene in the saving process, e.g. you can change the value of fields based on whether the mail sending was successful or not save the instance at all.
The solution to what you want to do is to use Django Signals. By using Signals you can hook code to when a model is created and saved without having to rewrite the save method, that keep the separation of code and logic in a much nicer way, obviously the model does not need to know about the emails for example.
An example of how to use Signals would be to simply do the following:
from django.db.models.signals import pre_save
from django.dispatch import receiver
from myapp.models import MyModel
#receiver(pre_save, sender=MyModel)
def my_handler(sender, **kwargs):
# Code to execute whenever MyModel is saved...
If you still want to override the save() method you can use the Python super() method to do so (docs).
class MyModel(models.Model):
def save(self, *args, **kwargs):
# This will call the parent method that you are overriding
# so it will save your instance with the default behavior.
super(MyModel, self).save(*args, **kwargs)
# Then we add whatever extra code we want, e.g. send email...
Messenger.send_email()
You need to activate signal once your message is saved. That means, when your message is saved, django will issue signal as follows:
from django.db.models.signals import post_save
from django.dispatch import receiver
class Message(models.Model):
# fields...
# method for sending email
#receiver(post_save, sender=Message, dispatch_uid="send_email")
def send_email(sender, instance, **kwargs):
# your email send logic here..
You can put your signals in signals.py file inside your app folder and make sure to import that in your application config file as follows:
message/apps.py
from django.apps import AppConfig
class MyAppConfig(AppConfig):
name = 'message'
def ready(self):
import message.signals
And update init file as follows:
message/__init__.py
default_app_config = 'message.apps.MyAppConfig'

Disable choice list in Django admin, only for editing

I want to disable some fields when I am editing an object. I have managed to do this for text fields, but it's been impossible for a dropdown list (choice list).
I am doing this action in the constructor of the form.
class OrderModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(forms.ModelForm, self).__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
if instance and instance.pk:
self.fields['description'].widget.attrs['readonly'] = True
self.fields['city_code'].widget.attrs['disabled'] = True
Notice how I made it for both with different keywords, but I can't do it for my customer_id field.
Setting the attribute to disabled or readonly only affects the way the widgets are displayed. It doesn't actually stop somebody submitting a post request that changes those fields.
It might be a better approach to override get_readonly_fields for your model.
class OrderModelAdmin(admin.Model
def get_readonly_fields(self, request, obj=None):
if self.obj.pk:
return ['description', 'city_code', 'customer']
else:
return []
The answer of #Alasdair is better than this one (because this one doesn't prevent a submission). But I post it, just in case someone wants the equivalent to 'readonly' for ModelChoiceField.
self.fields['customer_id'].widget.widget.attrs['disabled'] = 'disabled'
Notice, that for a ChoiceField is enought something like this:
self.fields['city_code'].widget.attrs['disabled'] = True

Django EventLog: Passing in current user

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

Django update instead of insert a new record

I having a weird issue with Django 1.3. When I try to add a new Activity using the ActivityForm, the Activity is often updated even if I explicitly create a new Activity object with not Id.
In addition, when the init of my ActivityForm class has an explicit trip_id parameter, the UI displays "Select a valid choice. That choice is not one of the available choices." for the field location (even though the selection shown is valid). However, if I make this an optional parameter and I pop it from kwargs, I don't see this issue.
Can someone please take a look at the code below and see what I am doing wrong?
forms.py
class DestinationMultipleChoiceField(ModelMultipleChoiceField):
def label_from_instance(self, obj):
return obj.city_name
class ActivityForm(forms.Form):
description = forms.CharField(max_length=100, required=True)
location = DestinationChoiceField(queryset = Visit.objects.none(), empty_label=None, required=True)
def __init__(self, trip_id = None, *args, **kwargs):
super(ActivityForm, self).__init__(*args, **kwargs)
if trip_id is not None:
self.fields['location'].queryset = Visit.objects.filter(trip=trip_id).all().select_related('destination')
# A new Activity() is created if nothing is provided
def save(self, activity = Activity()):
if not self.is_valid():
raise forms.ValidationError('ActivityForm was not validated first before trying to call save().')
activity.visit = self.cleaned_data['location']
activity.description = self.cleaned_data['description']
activity.added_by = User.objects.get(pk=1)
activity.save()
views.py
def add_activity(request, trip_id = None, template_name = 'edit-form.html'):
if request.POST:
form = ActivityForm(trip_id, request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('views.list_trip'))
else:
form = ActivityForm(trip_id)
return render_to_response(template_name, {
'page_title': 'Add',
'form': form,
}, context_instance=RequestContext(request))
Look at this line:
def save(self, activity = Activity()):
In Python, default parameters are evaluated once; thus, when the save method is called the first time, a new Activity will be created, but subsequent calls will use that Activity.
Change save to something like this:
def save(self, activity=None):
if activity is None:
activity = Activity()
# Rest of the method
Then, a new Activity will be created on every call (if one is not supplied as an argument).
Model instances are mutable. As such they should never be used as default arguments to a method, as you do in save. There's a reason why you've never seen that in any documentation or examples.
As noted in very many other questions on this site, default arguments are evaluated on definition, not execution. So each call to save with no activity argument will use the same Activity that was originally defined.
However, I don't understand why you're not using a ModelForm, which does most of this for you in any case.

Categories