Django bulk_update not working on default attributes - python

I want to change the default value of an attribute from a model in Django. So I want to update the existing values in the database. Strange enough, a bulk update doesn't change those values.
My model:
class UserSettings(models.Model):
offline_notification_filter = models.BooleanField(default=False)
My test
class TestSetOfflineNotificationMigration(APITestCase):
def test_set_offline_notification_filter_to_false(self):
user_settings_1 = UserSettingsFactory(offline_notification_filter=True)
user_settings_2 = UserSettingsFactory(offline_notification_filter=False)
user_settings_3 = UserSettingsFactory(offline_notification_filter=True)
user_settings_4 = UserSettingsFactory()
all_user_settings = UserSettings.objects.all()
for user_setting in all_user_settings:
user_setting.offline_notification_filter = False
UserSettings.objects.bulk_update(
all_user_settings, ["offline_notification_filter"]
)
self.assertEqual(user_settings_1.offline_notification_filter, False)
This test is failing because the the offlince_notification_filter is not updating. Anyone knows why not?

I think you are working with outdated instance, so you might need to user_settings_1.refresh_from_db().

bulk_update() is not necessarily needed in this case. Since this update operation is being performed directly on all objects of queryset, so just using update() will automatically make changes in a single operation.
Bulk update is more suited to operations where instances of different models are being updated.
However if you still prefer to use bulk_update, then here's the answer:
user_settings_1 is an in-memory instance here, whereas bulk_update operation has made the change in database. You need to refresh it from database.

As stated in the docs: https://docs.djangoproject.com/en/dev/ref/models/querysets/#django.db.models.query.QuerySet.bulk_update
You need to provide an array of the models to update, and specify the fields you want to update.
I would suggest the following code:
class TestSetOfflineNotificationMigration(APITestCase):
def test_set_offline_notification_filter_to_false(self):
user_settings_1 = UserSettingsFactory(offline_notification_filter=True)
user_settings_2 = UserSettingsFactory(offline_notification_filter=False)
user_settings_3 = UserSettingsFactory(offline_notification_filter=True)
user_settings_4 = UserSettingsFactory()
all_user_settings = UserSettings.objects.all()
# Create list to append new models
new_user_settings = []
for user_setting in all_user_settings:
# Create new model with the updated fields
new_user_setting = UserSettings(offline_notification_filter=False)
# Append
new_user_settings.append(new_user_setting)
# Response is amount of models updated
q = UserSettings.objects.bulk_update(
new_user_settings, ["offline_notification_filter"]
)
self.assertEqual(q, 4)
If you have a long set of fields to update, you can update the fields with the _meta method and create the list of fields to update like:
# Get list of fields
fields = [f.name for f in UserSettings._meta.fields]
# delete the ones you don't want to update with Sets
list_of_fields = fields - {"id"}
q = UserSettings.objects.bulk_update(
new_user_settings, list_of_fields
)

Related

Django import-export, only export one object with related objects

I have a form which enables a user to register on our website. Now I need to export all the data to excel, so I turned towards the import-export package. I have 3 models, Customer, Reference and Contact. The latter two both have a m2m with Customer. I also created Resources for these models. When I use Resource().export() at the end of my done() method in my form view, it exports all existing objects in the database, which is not what I want.
I tried googling this and only got one result, which basically says I need to use before_export(), but I can't find anywhere in the docs how it actually works.
I tried querying my customer manually like:
customer = Customer.objects.filter(pk=customer.id)
customer_data = CustomerResource().export(customer)
which works fine but then I'm stuck with the related references and contacts: reference_data = ReferenceResource().export(customer.references) gives me an TypeError saying 'ManyRelatedManager' object is not iterable. Which makes sense because export() expects an queryset, but I'm not sure if it's possible getting it that way.
Any help very appreciated!
One way is to override get_queryset(), you could potentially try to load all related data in a single query:
class ReferenceResource(resources.ModelResource):
def __init__(self, customer_id):
super().__init__()
self.customer_id = customer_id
def get_queryset(self):
qs = Customer.objects.filter(pk=self.customer.id)
# additional filtering here
return qs
class Meta:
model = Reference
# add fields as appropriate
fields = ('id', )
To handle m2m relationships, you may be able to modify the queryset to add these additional fields.
This isn't the complete answer but it may help you make progress.

Update an item using a django model instance

Is it a good idea to update an item by saving a model instance with the same id if the fields that are different between the item and the instance are unknown?
Lets say there is an Person item in the database:
id: 4
name: Foo
surename: Bar
tel: 0000000000
Is it a good idea to update that item like:
p = Person(
name='Foo'
surename='Bar'
tel='0000000111'
)
old_p = Person.objects.get(name='Foo', surname='Bar')
p.id = old_p.id
p.save()
Is it a good idea to update an item by saving a model instance with the same id?
No. If you Person model has other fields, you will need to copy all these fields in the new record p. Otherwise these might be updated to the default values the Person model has, or to the incorrect values you passed to the p = Person(…) call. This makes it hard to maintain code: each time you add a new field to a model, you need to update all code fragments where you update a single field.
If you want to update a single field, you can do this with an .update(…) call [Django-doc]:
Person.objects.filter(name='Foo', surname='Bar').update(tel='0000000111')
or if you want to run triggers, and work with the .save(…) method of that model, you can retrieve the object, and update that specific field, so:
p = Person.objects.get(name='Foo', surname='Bar')
p.tel = '0000000111'
p.save()

dependent multi object validation in django admin

Each "time range" entry of the TimeClass is dependent on each other.
They cannot overlap and start_time < end_time.
models.py
class Xyz(models.Model):
...
class TimeRangeClass(models.Model)
start_time = models.TimeField()
end_time = models.TimeField()
xyz = models.ForeignKey(Xyz)
# other fields here
def clean(self):
# Here I loop through TimeRangeClass.objects.all() and
# check for conflicts through my custom "my_validator_method".
# If there is a conflict I throw an error
#(I've since modified it to just be one single query as per Titusz advice)
for each in TimeRangeClass.objects.filter(xyz=self.xyz).exclude(id=self.id):
my_validator_method(start_time1=self.start_time,
end_time1=self.end_time,
start_time2=each.start_time,
end_time2=each.end_time)
admin.py
from .models import TimeRangeClass, Xyz
class TimeRangeClassInLine(admin.TabularInline):
model = TimeRangeClass
extra = 3
#admin.register(Xyz)
class Xyz(admin.ModelAdmin):
exclude = []
inlines = [TimeRangeClassInLine]
Problem: I can edit/add multiple TimeRangeClass's at once through the admin. But given that the models.Model clean method only evaluates 1 change at a time I can't validate multiple edits against each other.
Example:
Save an Entry1 & Entry2 without conflict
Change Entry2 to produce a validation error
Adjust Entry1 (instead of #2) so they do not overlap
This doesn't register because neither changes are written to the db.
I'm looking for a workaround.
Some hints on the problem:
You should not iterate over the full table when checking for overlapping rows. Just filter for the problematic rows... something like:
overlaps = TimeRangeClass.objects.filter(
Q(start_time__gte=self.start_time, start_time__lt=self.end_time) |
Q(end_time__gt=self.start_time, end_time__lte=self.end_time)
)
overlaps is now a queryset that evaluates when you iterate over it and only returns the conflicting objects.
If you are using Django with postgres you should check out https://docs.djangoproject.com/es/1.9/ref/contrib/postgres/fields/#datetimerangefield.
Once you have the conflicting objects you should be able to change their start and end times within the function and save the changes. Model.save() will not automatically call the model.clean() method. But be aware, if you save an object from the Django admin it will call the model.clean() method before saving.
So something like that:
def clean():
overlaps = TimeRangeClass.overlaps.for_trc(self)
for trc_object in overlaps:
fixed_object = fix_start_end(trc_object, self)
fixed_object.save()
If you feel brave you should also read up on transactions to make the mutation of multiple objects in the database all succeed or all fail and nothing in between.
def clean():
with transaction.atomic():
# do your multi object magic here ...
Update on clarified question:
If you want to validate or pre/process data that comes from admin inlines you have to hook into the corresponding ModelAdmin method(s). There are multiple ways to approach this. I guess the easiest would be to override ModelAdmin.save_fromset. Here you have access to all the inlineforms before they have been saved.

Nested chain vs duplicated information

There is a models.py with 4 model.
Its standard record is:
class Main(models.Model):
stuff = models.IntegerField()
class Second(models.Model):
nested = models.ForeignKey(Main)
stuff = models.IntegerField()
class Third(models.Model):
nested = models.ForeignKey(Second)
stuff = models.IntegerField()
class Last(models.Model):
nested = models.ForeignKey(Third)
stuff = models.IntegerField()
and there is another variant of Last model:
class Last(models.Model):
nested1 = models.ForeignKey(Main)
nested2 = models.ForeignKey(Second)
nested = models.ForeignKey(Third)
stuff = models.IntegerField()
Will that way save some database load?
The information in nested1 and nested2 will duplicate fields in Secod and Third and even it may become outdated ( fortunately not in my case, as the data will not be changed, only new is added ).
But from my thoughts it may save database load, when I'm looking all Last records for a certain Main record. Or when I'm looking only for Main.id for specific Last item.
Am I right?
Will it really save the load or there is a better practice?
It all depends how you access the data. By default Django will make another call to the database when you access a foreign key. So if you want to make less calls to the database, you can use select_related to prefetch the models in foreign keys.

I came across a tricky trouble about django Queryset

Tricky code:
user = User.objects.filter(id=123)
user[0].last_name = 'foo'
user[0].save() # Cannot be saved.
id(user[0]) # 32131
id(user[0]) # 44232 ( different )
user cannot be saved in this way.
Normal code:
user = User.objects.filter(id=123)
if user:
user[0].last_name = 'foo'
user[0].save() # Saved successfully.
id(user[0]) # 32131
id(user[0]) # 32131 ( same )
So, what is the problem?
In first variant your user queryset isn't evaluated yet. So every time you write user[0] ORM makes independent query to DB. In second variation queryset is evalutaed and acts like normal Python list.
And BTW if you want just one row, use get:
user = User.objects.get(id=123)
when you index into a queryset, django fetches the data (or looks in its cache) and creates a model instance for you. as you discovered with id(), each call creates a new instance. so while you can set the properties on these qs[0].last_name = 'foo', the subsequent call to qs[0].save() creates a new instance (with the original last_name) and saves that
i'm guessing your particular issue has to do with when django caches query results. when you are just indexing into the qs, nothing gets cached, but your call if users causes the entire (original) qs to be evaluated, and thus cached. so in that case each call to [0] retrieves the same model instance
Saving is possible, but everytime you access user[0], you actually get it from the database so it's unchanged.
Indeed, when you slice a Queryset, Django issues a SELECT ... FROM ... OFFSET ... LIMIT ... query to your database.
A Queryset is not a list, so if you want to it to behave like a list, you need to evaluate it, to do so, call list() on it.
user = list(User.objects.filter(id=123))
In your second example, calling if user will actually evaluate the queryset (get it from the database into your python program), so you then work with your Queryset's internal cache.
Alternatively, you can use u = user[0], edit that and then save, which will work.
Finally, you should actually be calling Queryset.get, not filter here, since you're using the unique key.

Categories

Resources