Django - NullBooleanField won't retain False value from custom save() method - python

Newcomer to Django, so forgive me if I'm missing something obvious or doing something stupid here...
I have a Django model with a custom save() extension. Trimming the unrelated fields and methods, it looks like this:
class Content(models.Model):
url = models.URLField(max_length=1000)
image = models.URLField(blank=True, max_length=1000)
image_type = models.NullBooleanField(default=None)
def save(self, *args, **kwargs):
if self.url:
img, img_type = image_extractor(self.url)
print 'extractor returned: ', img_type
print 'before set: ', self.image_type
setattr(self, 'image', img)
setattr(self, 'image_type', img_type)
print 'after set: ', self.image_type
super(Content, self).save(*args, **kwargs)
print 'from query: ', Content.objects.get(url=self.url).image_type
The image_extractor function returns a url string and a boolean value representing the image type: True for images larger than a certain width, False for smaller images, or None if no suitable image was found. The problem is, when I query Content.objects and check image_type, all objects return either True or None. Hence the print statements. The images with image_type None or True work as expected, but when image_type is returned False, this is what prints:
extractor returned: False
before set: None
after set: False
from query: True
Somehow the call to super().save always reverts image_type to True if I try to set False (but not if I set None). I admit, I don't understand super() very well, but based on the documentation it seems to be necessary when extending the model function. Can anyone explain what's going on here, and how I can fix it?
Edit: I notice that I can change the value to False through the admin page and it stays False. But I still need to fix the save() method. For now, I have just replaced the boolean field with an integer field and saved values as 0 or 1, which does work. But I'd still like to know why the more appropriate field doesn't.

Since writing this question, I had moved ahead with a temporary solution of using an IntegerField to store the boolean as 0 or 1. Upon deciding to revisit the problem tonight and switching back to the NullBooleanField, suddenly everything works as intended. So it seems danodonovan was correct: the code above is correct, and unfortunately I still have no idea what was causing the error. Something else I changed while using the IntegerField must have resolved the issue. Thanks to those of you who have taken a look and offered some opinions, and I'm sorry to have wasted your time.

the way you overload save method looks good.
I think you are not loading the good object from database.
Use the primary key will probably works better.
In your case just replace that:
Content.objects.get(url=self.url).image_type
by that:
Content.objects.get(pk=self.pk).image_type
Looks at the documentation: https://docs.djangoproject.com/en/dev/ref/models/instances/#the-pk-property
Extract from the django documention:
Once the object has been saved, you must reload the object in order to
access the actual value that was applied to the updated field:
product = Products.objects.get(pk=product.pk)
print(product.number_sold)

Related

Codenerix - Disable a dropdown field with foreign key using ng-readonly

Codenerix
Has anyone knows how to use correctly ng-readonly in a GenModelForm when coming from a sublist tab (GenList) who calls a GenCreateModal windows?
Structure is a master-detail, sublist tab has pk of master table and calls GenCreateModal with this pk argument of master table.
GenCreateModal receives pk argument in his asociated form (the mentioned GenModelForm) and can use it. The goal is to disable field with ng-disabled if pk argument of master table is filled. This way when create from another list of detail table without arguments, field can be filled with a value selecting it with the dropdown, and when coming from master table it cannot be modified and it will be assigned to master pk value.
I tried to do it that way:
First assign 'client' on GenCreateModal with:
def get_initial(self):
client = self.kwargs.get('pk', None)
if client:
self.kwargs['client'] = client
return self.kwargs
Then read it on the GenModelform with:
def __init__(self, *args, **kwargs):
super(DetailForm, self).__init__(*args, **kwargs)
if kwargs.get('initial', None) and kwargs['initial'].get('client', None):
self.fields['client'].widget.attrs[u'ng-readonly'] = 'true'
But it do not work with dropdown fields. Field can be modified.
Cause is that in templatetags_list.py of codenerix we have:
def inireadonly(attrs, i):
field = ngmodel(i)
return addattr(attrs, 'ng-readonly=readonly_{0}'.format(field))
This code set ng-readonly to "true readonly_client" instead of "true" when it comes with value "true" from GenModelForm, values are concatenated.
I found a workaround with:
self.fields['client'].widget.attrs[u'ng-readonly'] = 'true || '
this way the end value will be "true || readonly_client" that result on "true" as desired when evaluated, but I think it is not the proper way.
On my personal fork of django-codenerix I have changed the function to (functions is on two py files, should change both):
def inireadonly(attrs, i):
field = ngmodel(i)
if attrs.get('ng-readonly', None) is None:
attrs = addattr(attrs, 'ng-readonly=readonly_{0}'.format(field))
return attrs
This way it respects the value when it comes filled form GenModelForm, but I'm not sure about inconveniences and collateral effects. For example when want to concatenate conditions, with that change should read old value, concatenate manually and set new value. I think it should be a better way to do it and 'ng-readonly=readonly_{0}'.format(field) should have a functionality that I haven't discovered yet. Don't want to lose it when I discover it. So I revert the change and look for another solution.
Currently I'm using
self.fields['client'].widget.attrs[u'ng-disabled'] = 'true'
and it goes OK, I'm using this way and I have no problem now, but I'm curious about the way to use ng-readonly if I need it on the future. That's because with ng-readonly we can select text on fields with the mouse for example and can not select it with ng-disabled. In some cases it could be of interest.
Has anyone knows how to use ng-readonly in a correct way?
Has anyone knows the functionality of 'ng-readonly=readonly_{0}'.format(field)?
You can define an extra attribute to your fields in your forms. Add {'extra': ['ng-disabled=true']} in the field of your GenModelForm, inside your __groups__ method. Example:
def __groups__(self):
g = [
(_('Info'), 12,
['client', 6, {'extra': ['ng-disabled=true']}],
)
]
return g
You should use ng-disabled as you are doing. This is the way we do it in Django Codenerix Example # Gibhub (lines 41 and 42) and this is how it has been developed for StaticSelects (line 228) as well.

Duplicate/Archive entry in Django to another model

I have been at this for Two days and I'm now hoping someone can point me in the right direction. All I am trying to do is duplicate an entry in a table/model into another model with mirrored fields, essentially creating an archived version. I want this to happen when the user calls the update view.
What I have tried so far is setting pk to None and then trying to find a way to move the previous version to the mirrored/archive model. After a couple of hours of research I gave up on this path. Next I thought the answer would lie with the pre_save receiver but I can't find a way to access the model instance to then save that to the archive model.
#receiver(pre_save, sender=InstrumentAnnual)
def archive_calc_instance(sender, instance, **kwargs):
stored_id = getattr(instance, 'id', None)
e = InstrumentAnnual.objects.filter(id = stored_id)
archive = InstrumentAnnualArchive(e.field_name, e.another_field_name...)
archive.save()
As far as I can tell this should work however e only contains the First field from the model.
Is there something that can be done with this code to achieve my goal or is there a more 'Django' way to solve this? I.e. some sort of official archive feature?
Thanks in advance.
With the help of #Igor's comment I amended my solution to this:
def archive_calc(self, object_id):
annual = InstrumentAnnual.objects.get(id = object_id)
annual_archive = InstrumentAnnualArchive()
for field in annual._meta.fields:
setattr(annual_archive, field.name, getattr(annual, field.name))
annual_archive.pk = None
annual_archive.save()
It occured to me that using pre_save wouldn't work as it is listening/linked to a model, not a view as I originally thought. So I placed the above code in my Update View and called it passing the id in object_id.
Thanks again for the help.
You should be using named arguments in your constructor, otherwise the first argument will be interpreted as the id, so try:
# omitted code
e = InstrumentAnnual.objects.filter(id=stored_id)
archive = InstrumentalAnnualArchive(field_name=e.field_name, another_name=e.another_field_name, …)
archive.save()
But you could also use Django's create function, so:
# omitted code
e = InstrumentAnnual.objects.filter(id=stored_id)
archive = InstrumentalAnnualArchive.objects.create(field_name=e.field_name, another_name=e.another_field_name, …)
This way handles the save for you, so you don't need to explicitly save your object.

Django Datetime Issue

What I am trying isn't difficult, however it really isn't working. I cannot see where an error is. In my abstract user model I have an is_donator method that just does not work.
#property
def is_donator(self):
if(self.donator >= datetime.now()):
return True
else:
return False
Some reason it just does not return anything, it all looks alright to me though, any ideas?
You have two related issues. The first is you are using the wrong comparison.
if(self.donator >= datetime.now()):
This means that the donor must become a donor at some point in the future.
Change it to
if(self.donator <= datetime.now()):
This will ensure they became a donor in the past.
The other issue you have is that you are using auto_now:
Automatically set the field to now every time the object is saved.
Useful for “last-modified” timestamps. Note that the current date is
always used; it’s not just a default value that you can override.
This, then, relates to your first issue. Every time the user field is updated - if you don't explicitly set your field - it defaults to now.
Update based on comments: This is checking if your donator is not null and also ensure that it exists. If it doesn't exist, it is up to you to determine your logic, though if it doesn't exist, your user isn't a donator. You can probably just return False in your except block. This block is pretty verbose, but it shows you explicitly what is happening.
def is_donator(self):
try:
if self.donator: # This is checking that self.donator is not null
if(self.donator >= datetime.now()):
return True
else:
return False
else:
# return false? # This runs if self.donator is null
except NameError:
# do something (or just 'pass', because your self.donator value doesn't exist)

get_or_create django model with ManyToMany field

Suppose I have three django models:
class Section(models.Model):
name = models.CharField()
class Size(models.Model):
section = models.ForeignKey(Section)
size = models.IntegerField()
class Obj(models.Model):
name = models.CharField()
sizes = models.ManyToManyField(Size)
I would like to import a large amount of Obj data where many of the sizes fields will be identical. However, since Obj has a ManyToMany field, I can't just test for existence like I normally would. I would like to be able to do something like this:
try:
x = Obj(name='foo')
x.sizes.add(sizemodel1) # these can be looked up with get_or_create
...
x.sizes.add(sizemodelN) # these can be looked up with get_or_create
# Now test whether x already exists, so I don't add a duplicate
try:
Obj.objects.get(x)
except Obj.DoesNotExist:
x.save()
However, I'm not aware of a way to get an object this way, you have to just pass in keyword parameters, which don't work for ManyToManyFields.
Is there any good way I can do this? The only idea I've had is to build up a set of Q objects to pass to get:
myq = myq & Q(sizes__id=sizemodelN.id)
But I am not sure this will even work...
Use a through model and then .get() against that.
http://docs.djangoproject.com/en/dev/topics/db/models/#extra-fields-on-many-to-many-relationships
Once you have a through model, you can .get() or .filter() or .exists() to determine the existence of an object that you might otherwise want to create. Note that .get() is really intended for columns where unique is enforced by the DB - you might have better performance with .exists() for your purposes.
If this is too radical or inconvenient a solution, you can also just grab the ManyRelatedManager and iterate through to determine if the object exists:
object_sizes = obj.sizes.all()
exists = object_sizes.filter(id__in = some_bunch_of_size_object_ids_you_are_curious_about).exists()
if not exists:
(your creation code here)
Your example doesn't make much sense because you can't add m2m relationships before an x is saved, but it illustrated what you are trying to do pretty well. You have a list of Size objects created via get_or_create(), and want to create an Obj if no duplicate obj-size relationship exists?
Unfortunately, this is not possible very easily. Chaining Q(id=F) & Q(id=O) & Q(id=O) doesn't work for m2m.
You could certainly use Obj.objects.filter(size__in=Sizes) but that means you'd get a match for an Obj with 1 size in a huge list of sizes.
Check out this post for an __in exact question, answered by Malcolm, so I trust it quite a bit.
I wrote some python for fun that could take care of this.
This is a one time import right?
def has_exact_m2m_match(match_list):
"""
Get exact Obj m2m match
"""
if isinstance(match_list, QuerySet):
match_list = [x.id for x in match_list]
results = {}
match = set(match_list)
for obj, size in \
Obj.sizes.through.objects.filter(size__in=match).values_list('obj', 'size'):
# note: we are accessing the auto generated through model for the sizes m2m
try:
results[obj].append(size)
except KeyError:
results[obj] = [size]
return bool(filter(lambda x: set(x) == match, results.values()))
# filter any specific objects that have the exact same size IDs
# if there is a match, it means an Obj exists with exactly
# the sizes you provided to the function, no more.
sizes = [size1, size2, size3, sizeN...]
if has_exact_m2m_match(sizes):
x = Obj.objects.create(name=foo) # saves so you can use x.sizes.add
x.sizes.add(sizes)

Add an "empty" option to a ChoiceField based on model datas

I'm defining a ChoiceField based on a model's datas.
field = forms.ChoiceField(choices=[[r.id, r.name] for r in Model.objects.all()])
However I'd like to prepend my options with an empty one to select "no" objects.
But I can't find a nice way to prepend that.
All my tests like :
field = forms.ChoiceField(choices=[[0, '----------']].extend([[r.id, r.name] for r in Model.objects.all()]))
Returns me a "NoneType object not iterable" error.
The only way I've found until now is the following :
def append_empty(choices):
ret = [[0, '----------']]
for c in choices:
ret.append(c)
return ret
And when I define my field :
forms.ChoiceField(choices=append_empty([[r.id, r.name] for r in
Restaurant.objects.all()]), required=False)
However I'd like to keep my code clean and not have that kind of horrors.
Would you have an idea for me ? :p
Thanks by advance.
An easy answer is to do:
field = forms.ChoiceField(choices=[[0, '----------']] + [[r.id, r.name] for r in Model.objects.all()])
Unfortunately, your approach is flawed. Even with your 'working' approach, the field choices are defined when the form is defined, not when it is instantiated - so if you add elements to the Model table, they will not appear in the choices list.
You can avoid this by doing the allocation in the __init__ method of your Form.
However, there is a much easier approach. Rather than messing about with field choices dynamically, you should use the field that is specifically designed to provide choices from a model - ModelChoiceField. Not only does this get the list of model elements dynamically at instantiation, it already includes a blank choice by default. See the documentation.
Since this question and its answer almost solved a problem I just had I'd like to add something. For me, the id had to be empty because the model didn't recognise '0' as a valid option, but it accepted empty (null=True, blank=True). In the initializer:
self.fields['option_field'].choices = [
('', '------')] + [[r.id, r.name] for r in Model.objects.all()]

Categories