I'm using Django and a Postgres database and I have two tables (Authors, Books).
I don't want to enable to deletion of any records, instead to change the boolean of an unseen field called 'active' - a field both tables have. What i'd like to happen is when a user changes an Author's 'active' field to False, all of the books with a FK to this author in the Books table have their 'active' field's also set to False.
In Django, my only options it seems related to deletion, how would I set this up with the models.py file?
You can solve this with signals. In the following example I assume your Book model has a foreign key field that looks like this.
class Book(models.Model):
...
author = models.ForeignKey("Author", related_name="books")
...
Option 1. Use Signals
An example function hooked into the Author post_save signal.
from django.db import transaction
from django.db.models.signals import post_save
def update_books_status(sender, instance, created, **kwargs):
with transaction.atomic():
books = Books.objects.select_for_update().filter(author=instance)
for book in books:
book.active = False
book.save()
post_save.connect(update_books_status, sender=Author)
In this example the atomic transaction ensures that all the updates are run in a single DB query. This saves you from the n+1 issue you would have if you just ran through the query set in a for loop.
Option 2. Override save()
Another way to handle this that is probably better is to override the Author model save() method.
class Author(models.Model):
...
def save(self, *args, **kwargs):
# Make sure we are not saving a new Author
# And we are saving the Author as inactive
# And we are are saving an Author that was active.
if not self._state.adding and not self.active and self._loaded_values['active']:
self.inactivate_books()
super().save(*args, **kwargs)
def inactivate_books(self):
with transaction.atomic():
books = Books.objects.select_for_update().filter(author=self)
for book in books:
book.active = False
book.save()
Related
I have the following models:
class Item(models.Model):
# fields
# ...
class Collection(models.Model):
items = models.ManyToManyField(Item, related_name="collections")
# other fields
# ...
Now I want two things:
I want to control if an Item can be added to a Collection.
I want the Collection to update some of its fields if an Item was added or removed.
For the second issue I know that there is the django.db.models.signals.m2m_changed which I can use to hook into changes of the relation. Is it allowed/ok to change the Collection within the signal callback? Can I use the signal also for "aborting" the insertion for issue 1?
I think the best way to approach both of your desired behaviors is not with signals, but rather with an overridden save() and delete() method on the through table which you would define explicitly using the argument through see: https://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.ManyToManyField.through. and this: https://docs.djangoproject.com/en/dev/topics/db/models/#overriding-predefined-model-methods
Something like this:
# -*- coding: utf-8 -*-
from django.db import models
class Item(models.Model):
# fields
# ...
class Collection(models.Model):
items = models.ManyToManyField(Item, related_name="collections", through="CollectionItem")
# other fields
# ...
class CollectionItem(models.Model):
collection = models.ForeignKey(Collection)
item = models.ForeignKey(Item)
def save(self, *args, **kwargs):
# Only allow this relationship to be created on some_condition
# Part 1 of your question.
if some_condition:
super(CollectionItem, self).save(*args, **kwargs)
# Update some fields on Collection when this
# relationship is created
# Part 2 of your question (1/2)
self.Collection.updateSomeFields()
def delete(self, *args, **kwargs):
collection = self.collection
super(CollectionItem, self).delete(*args, **kwargs)
# Update some fields on Collection when this relationship
# is destroyed.
# Part 2 of your question (2/2)
collection.updateSomeFields()
Incidentally, you'll find that adding a relationship will cause a save-signal on this through model.
And, regarding signals, once you have the through table in place, you'd be able to listen for pre_save and/or post_save signals, but neither of them will allow you to directly veto the creation of the relationship.
If one or both of your models are supplied by a 3rd party and you really cannot create the through table, then, yes, the signal route may be the only way to go.
https://docs.djangoproject.com/en/dev/ref/signals/#m2m-changed
In which case, you could listen for the m2m_changed event and trigger updates to your collection objects (part 2 of your question) and retroactively delete inappropriately created relationships (part 1 of your question). However, as this latter bit is a fugly kludgy, I'd stick with the explicit through table if you can.
The pre_save signal is called before saving an instance. But you are not able to abort the save operation from there. A better solution would be to add a new method to your Collection model, which is responsible for checking if an Item can be added:
class Collection(models.Model):
items = models.ManyToManyField(Item, related_name="collections")
...
def add_item(self, item):
if check_if_item_can_be_added(item):
items.add(item)
self.save()
def check_if_item_can_be_added(self, item):
# do your checks here
When adding an instance to a m2m field, the save method does not get called. You are right, the m2m_changed signal is the way to go. You can safely update the collection instance in there.
I want to do an extra initalization whenever instances of a specific django model are created. I know that overriding __init__ can lead to trouble. What other alternatives should I consider?
Update. Additional details: The intent is to initialize a state-machine that the instances of that model represent. This state-machine is provided by an imported library, and it's inner state is persisted by my django-model. The idea is that whenever the model is loaded, the state machine would be automatically initialized with the model's data.
Overriding __init__ might work, but it's bad idea and it's not the Django way.
The proper way of doing it in Django is using signals.
The ones that are of interest to you in this case are pre_init and post_init.
django.db.models.signals.pre_init
Whenever you instantiate a Django
model, this signal is sent at the beginning of the model’s __init__()
method.
django.db.models.signals.post_init
Like pre_init, but this one is sent
when the __init__(): method finishes
So your code should be something like
from django.db import models
from django.db.models.signals import post_init
class MyModel(models.Model):
# normal model definition...
def extraInitForMyModel(**kwargs):
instance = kwargs.get('instance')
do_whatever_you_need_with(instance)
post_init.connect(extraInitForMyModel, MyModel)
You can as well connect signals to Django's predefined models.
While I agree that there often is a better approach than overriding the __init__ for what you want to do, it is possible and there might be cases where it could be useful.
Here is an example on how to correctly override the __init__ method of a model without interfering with Django's internal logic:
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=100)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# add your own logic
The two suggested methods in the docs rely on the instance being created in an arbitrary way:
Add a classmethod on the model class:
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=100)
#classmethod
def create(cls, title):
book = cls(title=title)
# do something with the book
return book
book = Book.create("Pride and Prejudice")
Add a method on a custom manager:
class BookManager(models.Manager):
def create_book(self, title):
book = self.create(title=title)
# do something with the book
return book
class Book(models.Model):
title = models.CharField(max_length=100)
objects = BookManager()
book = Book.objects.create_book("Pride and Prejudice")
If that is your case, I would go that way. If not, I would stick to #vartec's answer.
i have a simple model:
class Article(models.Model):
name = models.CharField(max_length=1000)
custom_name = models.CharField(max_length=1000)
custom function:
def process_text(my_string):
return len(my_string)
i want the following:
custom_name = process_text(name)
Suppose the admin enters name as Mark Pilgrim then custom_name should have the auto populated value of 12.
in the admin.py can i have something like
prepopulated_fields
what would be an easy way to go about it.
Thanks!!
The easiest way to do this is to add a method that listens on the pre_save signal.
Here is a sample you can use (this code goes in your models.py for the app)
from django.db.models.signals import pre_save
from django.dispatch import receiver
# Your models go here
def process_text(mystring):
return len(mystring)
#receiver(pre_save, sender=Article)
def my_handler(sender, **kwargs):
if not kwargs['raw']:
obj = kwargs['instance']
obj.custom_name = process_string(obj.name)
The signals documentation has more information on signals, and the pre_save documentation lists what arguments the method expects.
If you have form with those fields then you can use some javascript to update the second field when the first lost focus. Then when someone will enter something in to the first field and leave the field, js can calculate length and put the value in second field.
If this should be done in the backend level - then you can for example override model's save() method. More here - https://docs.djangoproject.com/en/dev/topics/db/models/#overriding-predefined-model-methods
Cheers.
In my opinion the best option is to set the value on the form validation stage.
While subclassing db.models.Model, sometimes it's essential to add extra checks/constraints.
For example, I have an Event model with start_date and end_date: I want to add validation into the fields or the model so that end_date > start_date.
At least I know this can be done outside the models.Model inside the ModelForm validation. But how to attach to the fields and the models.Model?
I would not put constraints like these in the save method, it's too late. Raising an exception there, doesn't help the user who entered the data in the wrong way, because it will end up as a 500 and the user won't get the form with errors back etc.
You should really check for this in the Forms/ModelForms clean method and raise a ValidationError, so form.is_valid() returns false and you can send the errors in the form back to the user for correction.
Also note that since version 1.2, Django has had Model Validation.
It would look something like this:
class Foo(models.Model):
# ... model stuff...
def clean(self):
if self.start_date > self.end_date:
raise ValidationError('Start date is after end date')
As of Django 2.2, database level constraints are supported:
from django.db import models
from django.db.models import CheckConstraint, Q, F
class Event(models.Model):
start_date = models.DatetimeField()
end_date = models.DatetimeField()
class Meta:
constraints = [
CheckConstraint(
check = Q(end_date__gt=F('start_date')),
name = 'check_start_date',
),
]
Do it inside your save method of your model:
def save(self, *args, **kwargs):
if(self.end_date > self.start_date):
super(Foo, self).save(*args, **kwargs)
else:
raise Exception, "end_date should be greater than start_date"
As #stefanw says, it's better user experience to check in the form's clean method.
This is enough if you're very sure that there isn't, and never will be, another way to change the value. But since you can rarely be sure of that, if database consistency is important, you can add another check (in addition to the form), one of:
The easier and database-independent way is in the model's save method as #umnik700 said. Note that this still doesn't prevent other users of the database (another app, or the admin interface) from creating an inconsistent state.
To be 'completely' sure the database is consistent, you can add a database level constraint. E.g. you can create a migration with RunSQL and SQL, something like (not tested):
migrations.RunSQL('ALTER TABLE app_event ADD CONSTRAINT chronology CHECK (start_date > end_date);')
(Not tested). This may be database dependent, which is a downside of course.
In your example, it's probably not worth it (incorrect start/end times just look a bit weird, but affect only the one inconsistent event), and you don't want manual schema changes. But it's useful in cases where consistency is critical.
EDIT: You can also just save the start time and the duration, instead of the start and end times.
As of today, both postgres 9.4 and MS SQL Server >= 2008 support check constraints in sql. On top of this, there is django issue 11964 which seems to be ready for review since yesterday, so hopefully we'll see this integrated into django 2. The project rapilabs/django-db-constraints seems to implement this too.
Summarizing the answers from before, here is a complete solution I used for a project:
from django.db import models
from django.db.models import CheckConstraint, Q, F
from django.utils.translation import gettext_lazy as _
class Event(models.Model):
start_date = models.DatetimeField()
end_date = models.DatetimeField()
class Meta:
constraints = [
# Ensures constraint on DB level, raises IntegrityError (500 on debug=False)
CheckConstraint(
check=Q(end_date__gt=F('start_date')), name='check_start_date',
),
]
def clean(self):
# Ensures constraint on model level, raises ValidationError
if self.start_date > self.end_date:
# raise error for field
raise ValidationError({'end_date': _('End date cannot be smaller then start date.')})
Too bad there is no django.core.validators that can handle this :(
Given a model with ForeignKeyField (FKF) or ManyToManyField (MTMF) fields with a foreignkey to 'self' how can I prevent self (recursive) selection within the Django Admin (admin).
In short, it should be possible to prevent self (recursive) selection of a model instance in the admin. This applies when editing existing instances of a model, not creating new instances.
For example, take the following model for an article in a news app;
class Article(models.Model):
title = models.CharField(max_length=100)
slug = models.SlugField()
related_articles = models.ManyToManyField('self')
If there are 3 Article instances (title: a1-3), when editing an existing Article instance via the admin the related_articles field is represented by default by a html (multiple)select box which provides a list of ALL articles (Article.objects.all()). The user should only see and be able to select Article instances other than itself, e.g. When editing Article a1, related_articles available to select = a2, a3.
I can currently see 3 potential to ways to do this, in order of decreasing preference;
Provide a way to set the queryset providing available choices in the admin form field for the related_articles (via an exclude query filter, e.g. Article.objects.filter(~Q(id__iexact=self.id)) to exclude the current instance being edited from the list of related_articles a user can see and select from. Creation/setting of the queryset to use could occur within the constructor (__init__) of a custom Article ModelForm, or, via some kind of dynamic limit_choices_to Model option. This would require a way to grab the instance being edited to use for filtering.
Override the save_model function of the Article Model or ModelAdmin class to check for and remove itself from the related_articles before saving the instance. This still means that admin users can see and select all articles including the instance being edited (for existing articles).
Filter out self references when required for use outside the admin, e.g. templates.
The ideal solution (1) is currently possible to do via custom model forms outside of the admin as it's possible to pass in a filtered queryset variable for the instance being edited to the model form constructor. Question is, can you get at the Article instance, i.e. 'self' being edited the admin before the form is created to do the same thing.
It could be I am going about this the wrong way, but if your allowed to define a FKF / MTMF to the same model then there should be a way to have the admin - do the right thing - and prevent a user from selecting itself by excluding it in the list of available choices.
Note: Solution 2 and 3 are possible to do now and are provided to try and avoid getting these as answers, ideally i'd like to get an answer to solution 1.
Carl is correct, here's a cut and paste code sample that would go in admin.py
I find navigating the Django relationships can be tricky if you don't have a solid grasp, and a living example can be worth 1000 time more than a "go read this" (not that you don't need to understand what is happening).
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
self.fields['myManyToManyField'].queryset = MyModel.objects.exclude(
id__exact=self.instance.id)
You can use a custom ModelForm in the admin (by setting the "form" attribute of your ModelAdmin subclass). So you do it the same way in the admin as you would anywhere else.
You can also override the get_form method of the ModelAdmin like so:
def get_form(self, request, obj=None, **kwargs):
"""
Modify the fields in the form that are self-referential by
removing self instance from queryset
"""
form = super().get_form(request, obj=None, **kwargs)
# obj won't exist yet for create page
if obj:
# Finds fieldnames of related fields whose model is self
rmself_fields = [f.name for f in self.model._meta.get_fields() if (
f.concrete and f.is_relation and f.related_model is self.model)]
for fieldname in rmself_fields:
form.base_fields[fieldname]._queryset =
form.base_fields[fieldname]._queryset.exclude(id=obj.id)
return form
Note that this is a on-size-fits-all solution that automatically finds self-referencing model fields and removes self from all of them :-)
I like the solution of checking at save() time:
def save(self, *args, **kwargs):
# call full_clean() that in turn will call clean()
self.full_clean()
return super().save(*args, **kwargs)
def clean(self):
obj = self
parents = set()
while obj is not None:
if obj in parents:
raise ValidationError('Loop error', code='infinite_loop')
parents.add(obj)
obj = obj.parent