I wish to do the following. When a new project is created , i want to notify everybody that was assigned to the project that there's a new project available.
Here is my simplified project model:
class SalesProject(models.Model):
sales_project_name = models.CharField(max_length=100)
userProfile = models.ManyToManyField('UserProfile', blank=True)
history = HistoricalRecords(excluded_fields=['version', 'project_status'])
def __str__(self):
return self.sales_project_name
When a project is being created, i will send out the following signal :
def CreateProjectNotification(sender, **kwargs):
if kwargs['action'] == "post_add" and kwargs["model"] == UserProfile:
for person in kwargs['instance'].userProfile.all():
#some check here to prevent me from creating a notification for the creator or the project
Notifications.objects.create(
target= person.user,
extra = 'Sales Project',
object_url = '/project/detail/' + str(kwargs['instance'].pk) + '/',
title = 'New Sales Project created')
The reason why im using the m2m_changed.connect instead of post_save is because i wish to access the M2M field , UserProfile to send out the notifications. Since the object would not be added to the through table at the point of creation , i can't use the post_save and instead i have to track the changes from the through table .
problem
With that said , this signal runs as long as the save() function is called and the model which changed was the UserProfile model .
This is problematic as for example , i don't wish to send this same message when a new user was added. Instead i wish to run a separate signal to handle for that.
Is there a way , other than using if else to distinguish between a creation of the object and adding of a related M2M object?
You should avoid using signals as much as possible as they are bad practise as explained here.
You should override the save() function of your SalesProject model and handle any logic in there. It is possible to check if a SalesProject is created or updated by using self._state.adding.
self._state.adding == True means the SalesProject is created
self._state.adding == False means the SalesProject is updated
Thus to achieve what you want:
class SalesProject(models.Model):
sales_project_name = models.CharField(max_length=100)
userProfile = models.ManyToManyField('UserProfile', blank=True)
history = HistoricalRecords(excluded_fields=['version', 'project_status'])
def save(self, *args, **kwargs):
if self._state.adding: # check if object is created and not updated
# your logic goes here i.e. create/delete (related) objects
return super(SalesProject, self).save(*args, **kwargs)
def __str__(self):
return self.sales_project_name
EDIT
Lets say you create an UserProfile object and want to add it as a relation to a SalesProject object by assigning it to its userProfile field. To get any values of a newly created UserProfile object, it has to be saved to the database first.
An example:
# SalesProject model class
def save(self, *args, **kwargs):
if self._state.adding:
user_profile = UserProfile.objects.create(someField=someValue)
self.userProfile.add(user_profile)
return super(SalesProject, self).save(*args, **kwargs)
The UserProfile.objects.create() will create a new UserProfile with the values that you assign to the fields and save it to the database.
self.userProfile.add(user_profile) adds the newly created and saved UserProfile as a relation to the ManyToManyField of the SalesProject that was just created.
Related
I want to know is there any way where we can get the updated many to many field in one model using signals and post them to another model
class Post(models.Model):
name=models.CharField(max_length=24)
nc=models.ManyToManyField(settings.AUTH_USER_MODEL)
def __str__(self):
return self.name
# return self.name
m2m_changed.connect(receiver=like_post,sender=Post.nc.through)
I want to collect the updated record in the nc field of post model and using signals I want to create an object using function
here is the signal that connects to Post model
def like_post(sender, *args, **kwargs):
# if kwargs['action'] in ('post_update'):
if kwargs['action'] in ('post_add', 'post_remove','post_update'):
print(kwargs['instance'])
instance = kwargs['instance']
print(instance)
notify = Notify.objects.create(
recipient=instance,
creator=Post.objects.get(pk=50),
state='unread',
type=kwargs['action'],
)
else:
print('no instance')
in the recipient and the creator section I want to update those fields with an existing user object the creator is the person who updated the manytomanyfield and the recipient is the person who created that post
notify model:
class Notify(models.Model):
recipient = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='notify_recipient',on_delete=models.CASCADE)
creator = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='notify_sender',on_delete=models.CASCADE)
state = ('read', 'unread', 'unseen')
type = models.CharField(max_length=30, blank=True)
url = models.CharField(max_length=50, blank=True)
whenever I run this the instance just prints the post object name and fires this error
ValueError at /admin/posts/post/50/change/
Cannot assign "<Post: ok>": "Notify.recipient" must be a "User" instance.
You can see that your Notify class defines receipent as a ForeignKey element to the AUTH_USER_MODEL, but you are creating a Notify in your signal as:
notify = Notify.objects.create(
recipient=instance,
creator=Post.objects.get(pk=50),
state='unread',
type=kwargs['action'],
)
Here, the instance is an instance of Post rather than User, also you are using post instance in the creator field too. This is what causes the error.
To solve this error, you need to pass the user instance in those fields. For example, you can use something like:
notify = Notify.objects.create(
recipient=instance.nc, # find out how to pass the user of the post here
creator=Post.objects.get(pk=50), # replace by an user instance here instead of a post instance
state='unread',
type=kwargs['action'],
)
EDIT:
To make sure that the user instance is saved you need to override the save_model method of your ModelAdmin for post model as:
class PostAdmin(admin.ModelAdmin):
def save_related(self, request, form, formsets, change):
if not form.cleaned_data['nc']:
form.cleaned_data['nc'] = [request.user]
form.save_m2m()
Example:
class MyUser(models.Model):
blocked_users = models.ManyToManyField("self", blank=True, null=True)
user = MyUser.object.get(pk=1)
user.blocked_users.add(user)
user.blocked_users.all()[0] == user # (!!!)
Can It be prevented on model/db level? Or we need just do check somewhere in app.
Looking at the Django docs for ManyToManyField arguments, it does not seem possible.
The closest argument to what you want is the limit_choices_to However, that only limits choices on ModelForms and admin (you can still save it like you did in your example), and there is currently no easy way to use it to limit based on another value (pk) in the current model.
If you want to prevent it from happening altogether, you'll have to resort to overriding the save method on the through model--something like:
class MyUser(models.Model):
blocked_users = models.ManyToManyField(..., through="BlockedUser")
class BlockedUser(models.Model):
user = models.ForeignKey(MyUser)
blocked = models.ForeignKey(MyUser)
def save(self, *args, **kwargs):
# Only allow this relationship to be created if
if self.user != self.blocked:
super(BlockedUser, self).save(*args, **kwargs)
You could of course also do this with signals.
I am using Python 2.7 and Django 1.6.3
I want to define extra model field which is not actually in db table. I have a way which is defining a callable method with property annotation like;
class MyClass(models.Model):
my_field = models.CharField(max_length=50)
#property
def my_extra_field(self):
return self.my_field+'extra value'
This works fine to show it on admin change list pages. But the extra field is not on db level. It is being generated on programming level. Django asks it for every model object.
This cause me some troubles. My all admin change list pages have capability of exporting as excel or some other type. I am using admin query set to build that report. I have also jasper reports mechanism that works with SQL select queries. So, I, want to use the queryset to take this select query.
I think being able to define extra fields on db level is important for something. Not just for reason of mine. So, the question all about this.
Is there a way to define an extra custom fields on db level instead of programming level in Django.
Thank you!.
Edited
Adding it to admin list_filter is also another problem if it is not really a field. Django does not allow you to add it.
Could you create a new database field and then overwrite the save method to populate that field? I do that often to create a marked up version of a text field. For example:
class Dummmy(models.Model):
content = models.TextField()
content_html = models.TextField(editable=False, null=True)
def save(self, *args, **kwargs):
self.content_html = markdown(self.content)
super(Dummmy, self).save(*args, **kwargs)
So for you:
class MyClass(models.Model):
my_field = models.CharField(max_length=50)
my_extra_field = models.CharField(editable=False, null=True)
def save(self, *args, **kwargs):
self.my_extra_field = self.my_field + 'extra value'
super(MyClass, self).save(*args, **kwargs)
What would be the best way to create a "Update history" field in my Django Model so that I can keep track of multiple update dates?
I can create a model with last_updated = models.DateTimeField() and then have my view save the datetime to it, but what if I want to have a history of when the user has updated their post, and not just the most recent save?
Should it be a ManyToManyField or a CharField instead?
It's shouldn't be a field at all. Instead create a model, that will reference your main model using a ForeignKey:
class YourModel(models.Model):
name = models.CharField(max_length=100)
class YourModelUpdateHistory(models.Model):
your_model = models.ForeignKey('YourModel')
updated = models.DateTimeField()
This way you can have multiple dates for every model, while keeping the database properly normalized. It will also allow you in the future to add additional fields with other information about each update (for example who updated the object).
You should create a new YourModelUpdateHistory object whenever you update a YourModel object. You can even set it up so this is done automatically, thanks to the save() method (which is called by Django every time you save an object):
from django.utils import timezone
class YourModel(models.Model):
name = models.CharField(max_length=100)
def save(self, *args, **kwargs):
super(YourModel, self).save(*args, **kwargs)
YourModelUpdateHistory.objects.create(your_model=self, updated=timezone.now())
I have a model that has a user field that needs to be auto-populated from the currently logged in user. I can get it working as specified here if the user field is in a standard ModalAdmin, but if the model I'm working with is in an InlineModelAdmin and being saved from the record of another model inside the Admin, it won't take.
Here's what I think is the best solution. Took me a while to find it... this answer gave me the clues: https://stackoverflow.com/a/24462173/2453104
On your admin.py:
class YourInline(admin.TabularInline):
model = YourInlineModel
formset = YourInlineFormset
def get_formset(self, request, obj=None, **kwargs):
formset = super(YourInline, self).get_formset(request, obj, **kwargs)
formset.request = request
return formset
On your forms.py:
class YourInlineFormset(forms.models.BaseInlineFormSet):
def save_new(self, form, commit=True):
obj = super(YourInlineFormset, self).save_new(form, commit=False)
# here you can add anything you need from the request
obj.user = self.request.user
if commit:
obj.save()
return obj
I know I'm late to the party, but here's my situation and what I came up with, which might be useful to someone else in the future.
I have 4 inline models that need the currently logged in user.
2 as a created_by type field. (set once on creation)
and the 2 others as a closed_by type field. (only set on condition)
I used the answer provided by rafadev and made it into a simple mixin which enables me to specify the user field name elsewhere.
The generic formset in forms.py
from django.forms.models import BaseInlineFormSet
class SetCurrentUserFormset(forms.models.BaseInlineFormSet):
"""
This assume you're setting the 'request' and 'user_field' properties
before using this formset.
"""
def save_new(self, form, commit=True):
"""
This is called when a new instance is being created.
"""
obj = super(SetCurrentUserFormset, self).save_new(form, commit=False)
setattr(obj, self.user_field, self.request.user)
if commit:
obj.save()
return obj
def save_existing(self, form, instance, commit=True):
"""
This is called when updating an instance.
"""
obj = super(SetCurrentUserFormset, self).save_existing(form, instance, commit=False)
setattr(obj, self.user_field, self.request.user)
if commit:
obj.save()
return obj
Mixin class in your admin.py
class SetCurrentUserFormsetMixin(object):
"""
Use a generic formset which populates the 'user_field' model field
with the currently logged in user.
"""
formset = SetCurrentUserFormset
user_field = "user" # default user field name, override this to fit your model
def get_formset(self, request, obj=None, **kwargs):
formset = super(SetCurrentUserFormsetMixin, self).get_formset(request, obj, **kwargs)
formset.request = request
formset.user_field = self.user_field
return formset
How to use it
class YourModelInline(SetCurrentUserFormsetMixin, admin.TabularInline):
model = YourModel
fields = ['description', 'closing_user', 'closing_date']
readonly_fields = ('closing_user', 'closing_date')
user_field = 'closing_user' # overriding only if necessary
Be careful...
...as this mixin code will set the currently logged in user everytime for every user. If you need the field to be populated only on creation or on specific update, you need to deal with this in your model save method. Here are some examples:
class UserOnlyOnCreationExampleModel(models.Model):
# your fields
created_by = # user field...
comment = ...
def save(self, *args, **kwargs):
if not self.id:
# on creation, let the user field populate
self.date = datetime.today().date()
super(UserOnlyOnCreationExampleModel, self).save(*args, **kwargs)
else:
# on update, remove the user field from the list
super(UserOnlyOnCreationExampleModel, self).save(update_fields=['comment',], *args, **kwargs)
Or if you only need the user if a particular field is set (like boolean field closed) :
def save(self, *args, **kwargs):
if self.closed and self.closing_date is None:
self.closing_date = datetime.today().date()
# let the closing_user field set
elif not self.closed :
self.closing_date = None
self.closing_user = None # unset it otherwise
super(YourOtherModel, self).save(*args, **kwargs) # Call the "real" save() method.
This code could probably be made way more generic as I'm fairly new to python but that's what will be in my project for now.
Only the save_model for the model you're editing is executed, instead you will need to use the post_save signal to update inlined data.
(Not really a duplicate, but essentially the same question is being answered in Do inline model forms emmit post_save signals? (django))
I had a similar issue with a user field I was trying to populate in an inline model. In my case, the parent model also had the user field defined so I overrode save on the child model as follows:
class inline_model:
parent = models.ForeignKey(parent_model)
modified_by = models.ForeignKey(User,editable=False)
def save(self,*args,**kwargs):
self.modified_by = self.parent.modified_by
super(inline_model,self).save(*args,**kwargs)
The user field was originally auto-populated on the parent model by overriding save_model in the ModelAdmin for the parent model and assigning
obj.modified_by = request.user
Keep in mind that if you also have a stand-alone admin page for the child model you will need some other mechanism to keep the parent and child modified_by fields in sync (e.g. you could override save_model on the child ModelAdmin and update/save the modified_by field on the parent before calling save on the child).
I haven't found a good way to handle this if the user is not in the parent model. I don't know how to retrieve request.user using signals (e.g. post_save), but maybe someone else can give more detail on this.
Does the other model save the user? In that case you could use the post_save signal to add that information to the set of the inlined model.
Have you tried implementing custom validation in the admin as it is described in the documentation? Overriding the clean_user() function on the model form might do the trick for you.
Another, more involved option comes to mind. You could override the admin template that renders the change form. Overriding the change form would allow you to build a custom template tag that passes the logged in user to a ModelForm. You could then write a custom init function on the model form that sets the User automatically. This answer provides a good example on how to do that, as does the link on b-list you reference in the question.