Here is the classic mixin used to know when a Django object is created or modified:
class TimeStampable(models.Model):
created = models.DateTimeField(auto_now_add=True, editable=False)
modified = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
The problem (it's not really a problem for most of us I guess) is that created and modified fields are not equal at the first creation (there is a tiny delta between them).
How would you improve this mixin to solve that specific issue?
I checked django-model-utils source code but found nothing.
I guess we would need to override __init__ method?
If you want 2 timezone objects to be equal, they must be created the exact same instance in time. That is virtually impossible, especially when you make the call to timezone.now() in series.
Essentially what happens at TimeStampable object creation time is that:
created gets a timezone.now instance.
modified gets a timezone.now which is created a tiny time fraction after the created's one.
We can override model's .save() method to solve this problem:
We will use model's _state.adding() method which is an instance of ModelState, to define if an object is yet unsaved (newly created).
If it is newly created, we need to take one (and only one) instance
of timezone.now and pass it to created and modified fields.
If the object just got modified, we must not forget to pass an instance of timezone.now to the modified field.
class MyTimestampableModel(Timestampable):
...
def save(self, *args, **kwargs):
timezone_now = timezone.now()
if self._state.adding:
self.created = timezone_now
self.modified = timezone_now
super(MyTimestampableModel, self).save(*args, **kwargs)
Related
I was thinking of creating an instance of a foreignkey field and referring it every time an instance of a model is created, but I didn't find any solution for this. Usually we need to create a model of foreignkey type and then refer to it from the model, but I want it to automatically create one foreignkey instance from the beginning of and always refer to it.
To provide an example let's say we've 2 model fields called User and WeeklySchedule. Everytime an instance of a User is created,another corresponding instance of a WeeklySchedule model is also created that will be referred to by the instance of the User.
We can do this inside save() method of the User model.
def save(self, *args, **kwargs):
schedule = create_and_or_get_new_weekly_schedule()
""" where create_and_or_get_new_weekly_schedule either creates a new
instance or gets that of the foreignkey model
"""
self.availability_schedule_tutor = schedule
super().save(*args, **kwargs)
We can also set the on_delete option of the foreignkey field to models.PROTECT or models.RESTRICT to make sure it never loses reference. Also make sure to set null=True or else an instance of a user can never be created.
Something like the following:
weekly_schedule = WeeklySchedule()
# Set your weekly_schedule fields here
weekly_schedule.save()
user = User()
# Set your user fields here
user.weekly_schedule = weekly_schedule
user.save()
Is it possible to add self - I mean the current object it's ManyToManyField?
class City(models.Model):
name = models.CharField(max_length=80)
country = models.ForeignKey('Country')
close_cities = models.ManyToManyField('City',blank=True, related_name='close_cities_set')
If I create let's say x = City.objects.create(...), I want the x to be a part of close_cities by default.
I can't find anything related to this problem. I tried to overwrite create() method but it did not worked.
After trying, I decided to create a signal which adds the city after creating an instance. Unfortunately this does not work but I can't figure out why. The signal is being called, the condition if created is True (checked).
#receiver(post_save,sender=myapp_models.City)
def add_self_into_many_to_many_field(sender, instance, created, **kwargs):
if created:
instance.close_cities.add(instance)
instance.save()
Do you know where is the problem?
In this case pre_save signal will be better solution.
In Your solution city.save calls add_self_into_many_to_many_field. instance.save calls add_self_into_many_to_many_field. and so on...
#receiver(pre_save, sender=myapp_models.City)
def add_self_into_many_to_many_field(sender, instance, **kwargs):
if instance.pk is None:
instance.close_cities.add(instance)
models.py:
class Car(models.Model):
...
class Pictures(models.Model):
car = models.ForeignKey(Car, related_name='pictures')
width = models.PositiveIntegerField(editable=False, default=780)
height = models.PositiveIntegerField(editable=False, default=585)
image = models.ImageField(upload_to = get_file_path, max_length=64, height_field='height', width_field='width')
def __unicode__(self):
return str(self.id)
def delete(self, *args, **kwargs):
storage, path = self.image.storage, self.image.path
super(Pictures, self).delete(*args, **kwargs)
storage.delete(path)
It works nice (I delete one picture from admin panel and this picture is automatically deleted from disk).
But when I deleted Car object through admin panel, images are not removed from disk.
How to fix that?
Thanks!
I'm sure the problem here is that the ORM uses ON DELETE CASCADE to have the database handle removing the relations, meaning your delete method won't get called.
You could probably just apply the same technique you used here and do:
class Car(models.Model):
...
def delete(self, *args, **kwargs):
for picture in self.pictures.all():
storage, path = picture.image.storage, picture.image.path
storage.delete(path)
super(Car, self).delete(*args, **kwargs)
However, you are better off using signals instead of overriding the delete methods https://docs.djangoproject.com/en/dev/ref/signals/#post-delete
Note that the delete() method for an object is not necessarily called when deleting objects in bulk using a QuerySet. To ensure customized delete logic gets executed, you can use pre_delete and/or post_delete signals.
I started looking into this and found some interesting things regarding the admin and deletion of objects.
When deleting an object from the admin, the following function is called,
django/contrib/admin/options.py -> delete_model()
which in turn calls obj.delete() with obj being the current object being deleted.
The delete method for an object then runs the following code,
collector = Collector(using=using)
collector.collect([self])
collector.delete()
The collector object now has an attribute 'data' which contains all of the related objects.
When collector.delete() gets run, it executes the function query.delete_batch(pk_list, self.using) which does a bulk deletion using the argument pk_list which is a list of primary keys for related objects being deleted. The bulk deletion function in turn doesn't call the delete method of each related object being deleted.
The good thing here is that the pre_save and post_save signals do get called for all related objects so we could move your custom deletion code into either a pre_save or post_save signal for the Pictures model.
This should work I'm thinking but I haven't had a chance to test.
I have the following models:
class Recipe(models.Model):
fields...
class Ingredient(models.Model):
fields...
class UsesIngredient(models.Model):
recipe = models.ForeignKey(Recipe)
ingredient = models.ForeignKey(Ingredient)
amount = models.FloatField()
group = models.CharField()
I have a view which lets the user add any number of 'UsesIngredient' models for a certain recipe through a dynamic formset. The group attribute is automatically filled in an hidden from the user.
The problem is that when the users adds a new form in the formset, but doesn't fill in any of the fields, I don't want that form saved. However, django still tries to save the form because the 'group' attribute has 'changed' (because it has been automatically filled in when the extra form was created).
Is there any way to get around this?
Thanks!
Well, I still didn't feel completely comfortable with Tim Edgar's solution, so I kept looking. I guess I found what I was looking for.
The 'Form' class, has two undocumented methods that are of use in this case: 'has_changed()' and '_get_changed_data'.
During ModelFormSet validation, every form checks 'has_changed()'. If the form did not changed, validation is skipped and a correct form is assumed.
Likewise, during ModelFormSet saving, the save_new_objects checks every form to see if it has changed. If it didn't change, the form isn't saved.
So my solution was to override the has_changed() method to return False if only the 'group' attribute has changed, and all other fields are empty. This is my implementation:
class UsesIngredientForm(forms.ModelForm):
class Meta:
model = UsesIngredient
def has_changed(self, *args, **kwargs):
self._get_changed_data(*args, **kwargs)
# If group is in changed_data, but no other fields are filled in, remove group so
# the form will not be validated or saved
if 'group' in self._changed_data and len(self._changed_data) == 1:
contains_data = False
for name in ['ingredient', 'amount', 'unit']:
field = self.fields[name]
prefixed_name = self.add_prefix(name)
data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
if data_value:
contains_data = True
break
if not contains_data:
self._changed_data.remove('group')
return bool(self._changed_data)
Hope this helps anybody in the future!
EDIT:
I edited this answer to reflect Tim Edgars comment.
I realize that this implementation still uses 'private' methods, but I haven't found a cleaner implementation using just the publicly documented methods. But then maybe that is just my own incompetence :).
You could try making all your fields to require a value by setting blank=False. See more here. It should require validation that the values that you care about are not left blank.
If that doesn't work, you can try creating your own custom save method that does the validation that you care about.
def save(self, *args, **kwargs):
# Do your checks on the properties such as self.group, self.amount, etc
# If it is fine then call
super(UsesIngredient, self).save(*args, **kwargs)
I was wondering if it was possible (and, if so, how) to chain together multiple managers to produce a query set that is affected by both of the individual managers. I'll explain the specific example that I'm working on:
I have multiple abstract model classes that I use to provide small, specific functionality to other models. Two of these models are a DeleteMixin and a GlobalMixin.
The DeleteMixin is defined as such:
class DeleteMixin(models.Model):
deleted = models.BooleanField(default=False)
objects = DeleteManager()
class Meta:
abstract = True
def delete(self):
self.deleted = True
self.save()
Basically it provides a pseudo-delete (the deleted flag) instead of actually deleting the object.
The GlobalMixin is defined as such:
class GlobalMixin(models.Model):
is_global = models.BooleanField(default=True)
objects = GlobalManager()
class Meta:
abstract = True
It allows any object to be defined as either a global object or a private object (such as a public/private blog post).
Both of these have their own managers that affect the queryset that is returned. My DeleteManager filters the queryset to only return results that have the deleted flag set to False, while the GlobalManager filters the queryset to only return results that are marked as global. Here is the declaration for both:
class DeleteManager(models.Manager):
def get_query_set(self):
return super(DeleteManager, self).get_query_set().filter(deleted=False)
class GlobalManager(models.Manager):
def globals(self):
return self.get_query_set().filter(is_global=1)
The desired functionality would be to have a model extend both of these abstract models and grant the ability to only return the results that are both non-deleted and global. I ran a test case on a model with 4 instances: one was global and non-deleted, one was global and deleted, one was non-global and non-deleted, and one was non-global and deleted. If I try to get result sets as such: SomeModel.objects.all(), I get instance 1 and 3 (the two non-deleted ones - great!). If I try SomeModel.objects.globals(), I get an error that DeleteManager doesn't have a globals (this is assuming my model declaration is as such: SomeModel(DeleteMixin, GlobalMixin). If I reverse the order, I don't get the error, but it doesn't filter out the deleted ones). If I change GlobalMixin to attach GlobalManager to globals instead of objects (so the new command would be SomeModel.globals.globals()), I get instances 1 and 2 (the two globals), while my intended result would be to only get instance 1 (the global, non-deleted one).
I wasn't sure if anyone had run into any situation similar to this and had come to a result. Either a way to make it work in my current thinking or a re-work that provides the functionality I'm after would be very much appreciated. I know this post has been a little long-winded. If any more explanation is needed, I would be glad to provide it.
Edit:
I have posted the eventual solution I used to this specific problem below. It is based on the link to Simon's custom QuerySetManager.
See this snippet on Djangosnippets: http://djangosnippets.org/snippets/734/
Instead of putting your custom methods in a manager, you subclass the queryset itself. It's very easy and works perfectly. The only issue I've had is with model inheritance, you always have to define the manager in model subclasses (just: "objects = QuerySetManager()" in the subclass), even though they will inherit the queryset. This will make more sense once you are using QuerySetManager.
Here is the specific solution to my problem using the custom QuerySetManager by Simon that Scott linked to.
from django.db import models
from django.contrib import admin
from django.db.models.query import QuerySet
from django.core.exceptions import FieldError
class MixinManager(models.Manager):
def get_query_set(self):
try:
return self.model.MixinQuerySet(self.model).filter(deleted=False)
except FieldError:
return self.model.MixinQuerySet(self.model)
class BaseMixin(models.Model):
admin = models.Manager()
objects = MixinManager()
class MixinQuerySet(QuerySet):
def globals(self):
try:
return self.filter(is_global=True)
except FieldError:
return self.all()
class Meta:
abstract = True
class DeleteMixin(BaseMixin):
deleted = models.BooleanField(default=False)
class Meta:
abstract = True
def delete(self):
self.deleted = True
self.save()
class GlobalMixin(BaseMixin):
is_global = models.BooleanField(default=True)
class Meta:
abstract = True
Any mixin in the future that wants to add extra functionality to the query set simply needs to extend BaseMixin (or have it somewhere in its heirarchy). Any time I try to filter the query set down, I wrapped it in a try-catch in case that field doesn't actually exist (ie, it doesn't extend that mixin). The global filter is invoked using globals(), while the delete filter is automatically invoked (if something is deleted, I never want it to show). Using this system allows for the following types of commands:
TemporaryModel.objects.all() # If extending DeleteMixin, no deleted instances are returned
TemporaryModel.objects.all().globals() # Filter out the private instances (non-global)
TemporaryModel.objects.filter(...) # Ditto about excluding deleteds
One thing to note is that the delete filter won't affect admin interfaces, because the default Manager is declared first (making it the default). I don't remember when they changed the admin to use Model._default_manager instead of Model.objects, but any deleted instances will still appear in the admin (in case you need to un-delete them).
I spent a while trying to come up with a way to build a nice factory to do this, but I'm running into a lot of problems with that.
The best I can suggest to you is to chain your inheritance. It's not very generic, so I'm not sure how useful it is, but all you would have to do is:
class GlobalMixin(DeleteMixin):
is_global = models.BooleanField(default=True)
objects = GlobalManager()
class Meta:
abstract = True
class GlobalManager(DeleteManager):
def globals(self):
return self.get_query_set().filter(is_global=1)
If you want something more generic, the best I can come up with is to define a base Mixin and Manager that redefines get_query_set() (I'm assuming you only want to do this once; things get pretty complicated otherwise) and then pass a list of fields you'd want added via Mixins.
It would look something like this (not tested at all):
class DeleteMixin(models.Model):
deleted = models.BooleanField(default=False)
class Meta:
abstract = True
def create_mixin(base_mixin, **kwargs):
class wrapper(base_mixin):
class Meta:
abstract = True
for k in kwargs.keys():
setattr(wrapper, k, kwargs[k])
return wrapper
class DeleteManager(models.Manager):
def get_query_set(self):
return super(DeleteManager, self).get_query_set().filter(deleted=False)
def create_manager(base_manager, **kwargs):
class wrapper(base_manager):
pass
for k in kwargs.keys():
setattr(wrapper, k, kwargs[k])
return wrapper
Ok, so this is ugly, but what does it get you? Essentially, it's the same solution, but much more dynamic, and a little more DRY, though more complex to read.
First you create your manager dynamically:
def globals(inst):
return inst.get_query_set().filter(is_global=1)
GlobalDeleteManager = create_manager(DeleteManager, globals=globals)
This creates a new manager which is a subclass of DeleteManager and has a method called globals.
Next, you create your mixin model:
GlobalDeleteMixin = create_mixin(DeleteMixin,
is_global=models.BooleanField(default=False),
objects = GlobalDeleteManager())
Like I said, it's ugly. But it means you don't have to redefine globals(). If you want a different type of manager to have globals(), you just call create_manager again with a different base. And you can add as many new methods as you like. Same for the manager, you just keep adding new functions that will return different querysets.
So, is this really practical? Maybe not. This answer is more an exercise in (ab)using Python's flexibility. I haven't tried using this, though I do use some of the underlying principals of dynamically extending classes to make things easier to access.
Let me know if anything is unclear and I'll update the answer.
Another option worth considering is the PassThroughManager:
https://django-model-utils.readthedocs.org/en/latest/managers.html#passthroughmanager
You should use QuerySet instead of Manager.
See Documentation here.