How to change the behaviour of unique true in django model? - python

Here I am not deleting the model objects from database. I am just changing the is_deleted status to True while delete. But while doing this unique=True gives error for the deleted objects so how can I handle this ?
I want to exclude is_deleted=True objects from unique True.
class MyModel(models.Model):
name = models.CharField(max_length=20, unique=True)
is_deleted = models.BooleanField(default=False)
#views
class MyViewSet(ModelViewSet):
serializer_class = MySerializer
queryset = MyModel.objects.all()
def destroy(self, request, *args, **kwargs):
object = self.get_object()
object.is_deleted = True
object.save()
return Response(status=status.HTTP_204_NO_CONTENT)

You can use a UniqueConstraint [Django docs] with a condition instead of the unique kwarg on the field. Although there is a caveat that validation (by forms etc.) will not be done automatically for a unique constraint with a condition and you will need to do that yourself.
from django.db.models import Q
class MyModel(models.Model):
name = models.CharField(max_length=20)
is_deleted = models.BooleanField(default=False)
class Meta:
constraints = [
models.UniqueConstraint(fields=['name'], condition=Q(is_deleted=False), name='unique_undeleted_name')
]
Note: Since Django 4.1 validation is automatically performed for all constraints using the Model.validate_constraints method and hence the above mentioned caveat doesn't apply.

Since django-2.2, you can work with Django's constraint API, you can then specify a UniqueConstraint [Django-doc] that has a condition:
class MyModel(models.Model):
# no unique=True ↓
name = models.CharField(max_length=20)
is_deleted = models.BooleanField(default=False)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['name'],
name='unique_name_not_deleted',
condition=Q(is_deleted=False)
)
]
It is of course the database that enforces this, and thus some databases might not have implemented that feature.

You may use following also:
class Meta:
unique_together = ("name", "is_deleted")

Related

Django: How to follow ForeignKey in unique_together constraint?

I have three Django Models:
a Property Model, most importantly having a slug field;
a Collection Model; and
a PropertyCollectionRelationship Model.
The last model models a ManyToMany relationship between the other two and is used as an explicit through of a ManyToManyField.
from django.db import models
class Property(models.Model):
slug = models.SlugField()
collections = models.ManyToManyField('Collection', blank=True, through='PropertyCollectionRelationship')
# ... other unimportant stuff omitted ...
class Collection(models.Model):
pass
# ... lots of stuff omitted ...
class PropertyCollectionRelationship(models.Model):
prop = models.ForeignKey(Property, on_delete=models.CASCADE)
coll = models.ForeignKey(Collection, on_delete=models.CASCADE)
I would like to add a uniqueness constraint which ensures that each collection has at most one property with any given slug. I tried to express this via a unique_together option in the Model Meta:
class PropertyCollectionRelationship(models.Model):
class Meta:
unique_together = [['coll', 'prop__slug']]
prop = models.ForeignKey(Property, on_delete=models.CASCADE)
coll = models.ForeignKey(Collection, on_delete=models.CASCADE)
This resulted in the following System Check Error:
SystemCheckError: System check identified some issues:
ERRORS:
myapp.PropertyCollectionRelationship: (models.E012) 'unique_together' refers to the nonexistent field 'prop__slug'.
How, if at all, can I achieve such a constraint?
Edit: As suggested below I could set unique=True on the slug field, however my use case requires different properties with the same slug to coexist.

How do I override the django admin LogEntry model

I want to change the model used for the default LogEntry class so it creates a varchar rather than a clob in the database for the attribute "object_id"
The original model is defined in
django/contrib/admin/models.py
class LogEntry(models.Model):
action_time = models.DateTimeField(_('action time'), auto_now=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL)
content_type = models.ForeignKey(ContentType, blank=True, null=True)
object_id = models.TextField(_('object id'), blank=True, null=True)
object_repr = models.CharField(_('object repr'), max_length=200)
action_flag = models.PositiveSmallIntegerField(_('action flag'))
change_message = models.TextField(_('change message'), blank=True)
I want to change the definition of object_id to
object_id = models.Charfield(_('object id'), max_length=1000, blank=True, null=True)
I'm aware that this could cause issues if someone defined a primary key on an entity which is larger than a varchar(1000), but I wouldn't want an entity with a PK defined like that so I'm happy with the limitation.
This will greatly improve the efficiency of the queries when accessing the history log.
I don't really want to hack the actual model definition, but I can't find out how to elegantly override the model definition.
Any ideas?
Django’s model fields provide an undocumented contribute_to_class method.
The other feature of Django we can use is the class_prepared signal.
from django.db.models import CharField
from django.db.models.signals import class_prepared
def add_field(sender, **kwargs):
"""
class_prepared signal handler that checks for the model named
MyModel as the sender, and adds a CharField
to it.
"""
if sender.__name__ == "MyModel":
field = CharField("New field", max_length=100)
field.contribute_to_class(sender, "new_field")
class_prepared.connect(add_field)
To override field you can simply delete original field from model:
from django.db.models import CharField
from django.db.models.signals import class_prepared
def override_field(sender, **kwargs):
if sender.__name__ == "LogEntry":
field = CharField('object id', max_length=1000, blank=True, null=True)
sender._meta.local_fields = [f for f in sender._meta.fields if f.name != "object_id"]
field.contribute_to_class(sender, "object_id")
class_prepared.connect(override_field)
I have just tested this solution by placing this code in __init__.py of my app. You will also need to write a custom migration:
from django.db import migrations, models
class Migration(migrations.Migration):
def __init__(self, name, app_label):
# overriding application operated upon
super(Migration, self).__init__(name, 'admin')
dependencies = [
('my_app', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='logentry',
name='object_id',
field=models.CharField('object id', max_length=1000, blank=True, null=True),
),
]
Looks like it works but use it on your own risk.
You can read more here.

Subclass a Django ManyRelatedManager a.k.a. ManyToManyField

Is there a way to Subclass a Django ManyRelatedManager a.k.a. ManyToManyField ?
The goal is to pre-filter all related models when calling the ManyRelatedManager by a flag of deleted=None. If deleted=None then it is a valid Model.
So far, this is the code, but it doesn't seem to work.
class ExcludeDeletedManyToManyField(models.ManyToManyField):
def get_queryset(self):
qs = super(ExcludeDeletedManyToManyField, self).get_queryset()
return qs.filter(deleted__isnull=True)
class SelfRefrencingModel(models.Model):
children = ExcludeDeletedManyToManyField('self', blank=True,
symmetrical=False, related_name='parents')
You can create proxy model of SelfRefrencingModel and override the default manager. Then use this proxy model in ManyToManyField.
Subclassing ManyToManyField will not help you because for the resulting queryset ManyRelatedManger is responsible.
Proxy model approach:
from django.db import models
class A(models.Model):
children = models.ManyToManyField('AProxy')
name = models.TextField()
deleted = models.NullBooleanField(null=True)
class FilterDeletedManager(models.Manager):
def get_queryset(self):
qs = super(FilterDeletedManager, self).get_query_set()
return qs.filter(deleted__isnull=True)
class AProxy(A):
objects = FilterDeletedManager()
class Meta:
proxy = True
Caveat with this approach is that now django expects AProxy instances for children field.
So maybe better readable and maintainable approach will be to add another attribute in __init__.
from django.db import models
class A(models.Model):
children = models.ManyToManyField('self')
name = models.TextField()
deleted = models.NullBooleanField(null=True)
def __init__(self, *args, **kwargs):
super(A, self).__init__(*args, **kwargs)
self.deleted_null_children = self.children.filter(deleted__isnull=True)
Here is my solution. #beezz, you may be correct to use a Proxy Model to do this, but I haven't used a Proxy Model before for this pattern, so this is how I solved this:
class SelfRefrencingQuerySet(models.query.QuerySet):
pass
class SelfRefrencingManager(BaseManager):
def get_queryset(self):
return SelfRefrencingQuerySet(self.model, self._db).filter(
deleted__isnull=True)
class SelfRefrencingBaseModel(models.Model):
children = models.ManyToManyField('self', blank=True, symmetrical=False,
related_name='parents')
# Manager
objects = SelfRefrencingManager()
objects_all = models.Manager() # So you still have acccess to the
# default Manager
If your intention is to use this with Django Admin or ModelForm; you don't need to subclass the ManyToManyField. See the django documentation
class SelfRefrencingModel(models.Model):
children = models.ManyToManyField('self', blank=True, symmetrical=False,
related_name='parents', limit_choices_to={'deleted': False}))
Note: If deleted is a BooleanField it has to be True or False. It can't be None/NULL.
beezz's idea of using a Proxy Model is also a good one.
What I sometimes do is to customise the default manager
class MyModelManager(models.Manager):
use_for_related_fields = True
def get_queryset(self):
qs = super(MyModelManager, self).get_queryset()
return qs.filter(deleted=False)
class MyModelManager(models.Model):
objects = MyModelManager()
_objects = models.Manger()
deleted = models.BooleanField(default=False)
By default deleted objects will be hidden, but if need them in your queryset, you can use _objects.

Why does Django admin list_select_related not work in this case?

I've got a ModelAdmin class that includes a foreign key field in its list_display. But the admin list page for that model is doing hundreds of queries, one query per row to get the data from the other table instead of a join (select_related()).
The Django docs indicate you can add list_select_related = True as an attribute to your ModelAdmin to make this go away, but it doesn't seem to work at all for me. This SO question seems to give a similar problem, but his resolution is unclear, and it doesn't work in my case.
Here's a cut-down version of my model and model admin:
class Device(models.Model):
serial_number = models.CharField(max_length=80, blank=True, unique=True)
label = models.CharField(max_length=80, blank=True)
def __str__(self):
s = str(self.serial_number)
if self.label:
s += ': {0}'.format(self.label)
return s
class Event(models.Model):
device = models.ForeignKey(Device, null=True)
type = models.CharField(max_length=40, null=False, blank=True, default='')
class EventAdmin(admin.ModelAdmin):
list_display = ('__str__', 'device')
list_select_related = True
However, adding that list_selected_related = True didn't change anything. I still get lots of queries like this instead of an SQL join:
Any ideas why the Django admin seems to be ignoring my list_select_related and doing N queries? I'm using Python 2.7 and Django 1.3.3.
The issue here is that setting list_select_related = True just adds a basic select_related() onto the query, but that call does not by default follow ForeignKeys with null=True. So the answer is to define the queryset the changelist uses yourself, and specify the FK to follow:
class EventAdmin(admin.ModelAdmin):
list_display = ('__str__', 'device')
def queryset(self, request):
return super(EventAdmin, self).queryset(request).select_related('device')
Since Django 1.6, list_select_related accepts a boolean, list or tuple with the names of the fields to include in the select_related() call.
Hence you can now use:
class EventAdmin(admin.ModelAdmin):
list_display = ('__str__', 'device')
list_select_related = ['device']
Although select_related is generally the way to go, there are time when one requires more control, then overiding the get_queryset becomes more applicable, this is a more modern version of Daniel Roseman's answer:
Where foo and bar are foreign key fields:
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('foo', 'foo__bar').only('foo__field1', 'foo__bar__field2')

Django ManyToMany Inlines Ordering in 1.2.x

I'm using Django 1.2's new ManyToMany admin.TabularInline to display related objects in the admin app, and it works great except I can't figure out what to set the "ordering" property to so it can sort by one of the cross-referenced field names.
For instance:
class Foo(models.Model):
name = models.CharField(max_length=100)
class Bar(models.Model):
title = models.CharField(max_length=100)
foos = models.ManyToManyField(Foo)
class FooBarInline(admin.TabularInline):
model = Bar.foos.through
ordering = ('name', ) # DOES NOT WORK
raw_id_fields = ('name', ) # THROWS EXCEPTION
class FooAdmin(admin.ModelAdmin):
inlines = (FooBarInline, )
class Meta:
model = Foo
How can I get to the Foo.name field to order by it in the inline?
The model ordering meta option designates the order of the inline elements.
class Foo(models.Model):
name = models.CharField(max_length=100)
class Meta:
ordering = ('name',)
If you needed to have the ordering of the admin model different from your primary ordering, you could do something like this:
class Foo_Extended(Foo):
class Meta:
ordering = ('name',)
And use Foo_Extended for your AdminInline model.
I'm assuming you know this, but Django 1.3 adds and ordering option to the InlineAdmin model but I know you said Django 1.2
I think you may override
ModelAdmin.formfield_for_foreignkey(self, db_field, request, **kwargs)
You can find details in the docs for ModelAdmin.formfield_for_foreignkey.

Categories

Resources