Django ORM, how to use values() and still work with choicefield?
Asked Answered
D

4

8

I am using django v1.10.2

I am trying to create dynamic reports whereby I store fields and conditions and the main ORM model information into database.

My code for the generation of the dynamic report is

class_object = class_for_name("app.models", main_model_name)

results = (class_object.objects.filter(**conditions_dict)
                               .values(*display_columns)
                               .order_by(*sort_columns)
                               [:50])

So main_model_name can be anything.
This works great except that sometimes associated models of the main_model have choicefield.

So for one of the reports main_model is Pallet.
Pallet has many PalletMovement.
My display columns are :serial_number, created_at, pallet_movement__location

The first two columns are fields that belong to Pallet model. The last one is from PalletMovement

What happens is that PalletMovement model looks like this:

class PalletMovement(models.Model):
    pallet = models.ForeignKey(Pallet, related_name='pallet_movements',
                               verbose_name=_('Pallet'))
    WAREHOUSE_CHOICES = (
        ('AB', 'AB-Delaware'),
        ('CD', 'CD-Delaware'),
    )
    location = models.CharField(choices=WAREHOUSE_CHOICES,
                                max_length=2,
                                default='AB',
                                verbose_name=_('Warehouse Location'))

Since the queryset will return me the raw values, how can I make use of the choicefield in PalletMovement model to ensure that the pallet_movement__location gives me the display of AB-Delaware or CD-Delaware?

Bear in mind that the main_model can be anything depending on what I store in the database.

Presumably, I can store more information in the database to help me do the filtering and presentation of data even better.

Dependent answered 5/8, 2017 at 7:10 Comment(1)
Hey, @KimStacks I was wondering, did you find any answer helpful?Blend
B
4

The values() method returns a dictionary of key-value pairs representing your field name and a corresponding value.

For example:

Model:

class MyModel(models.Model):
    name = models.CharField()
    surname = models.CharField()
    age = models.IntegerField()
    ...

Query:

result = MyModel.objects.filter(surname='moutafis').values('name', 'surname')

Result:

< Queryset [{'name': 'moutafis', 'surname': 'john'}] >

You can now manipulate this result as you would a normal dictionary:

if main_model_name is 'PalletMovement':
    # Make life easier
    choices = dict(PalletMovement.WAREHOUSE_CHOICES)

    for item in result:
        item.update({ 
            pallet_movement__location: verbal_choice.get(
                pallet_movement__location, pallet_movement__location)
        })

You can even make this into a function for better re-usability:

def verbalize_choices(choices_dict, queryset, search_key):
    result = queryset        

    for item in result:
        item.update({ search_key: choices_dict.get(search_key, search_key) })

    return result

verbal_result = verbalize_choices(
                    dict(PalletMovement.WAREHOUSE_CHOICES),
                    result,
                    'pallet_movement__location'
                )

I suggest the use of the update() and get() methods because they will save you from potential errors, like:

  • The search_key does not exist in the choice_dict then get() will return the value of the search_key
  • update() will try to update the given key-value pair if exists, else it will add it to the dictionary.

If the usage of the above will be in the template representation of your data, you can create a custom template filter instead:

@register.filter(name='verbalize_choice')
def choice_to_verbal(choice):
    return dict(PalletMovement.WAREHOUSE_CHOICES)[choice]

Have an extra look here: Django: How to access the display value of a ChoiceField in template given the actual value and the choices?

Blend answered 9/8, 2017 at 9:26 Comment(0)
I
1

You would use get_foo_display

In your template:

{{ obj.get_location_display }}

or

{{ obj.pallet_movement.get_location_display }}

[Edit:] As pointed out in the comments this will not work when calling values()

Itin answered 8/8, 2017 at 8:28 Comment(1)
Note you can't use get_foo_display if you use values().Entoil
D
1

an alternative to create a templatetag is :

{{form.choicefield.1}}

This shows the value of the initial data of the foreign key field instead the id.

Dilley answered 10/8, 2017 at 12:12 Comment(0)
H
1

The universal solution for any main_model_name is by Django Model _meta API introspection: class_object._meta.get_field(field_name).choices
That is:

choice_dicts = {}
for field_name in display_columns:
    choice_dicts[field_name] = {
        k: v for k, v in class_object._meta.get_field(field_name).choices
    }
out = []
for row in results:
    out.append({name: choice_dicts[name].get(value, value)
                for name, value in row.items()
                })

The rest is a trivial example, mostly copied code from the question

>>> pallet = app.models.Pallet.objects.create()
>>> palletm = app.models.PalletMovement.objects.create(pallet=pallet, location='AB')
>>>
>>> main_model_name = 'PalletMovement'
>>> conditions_dict = {}
>>> display_columns = ['pallet_id', 'location']
>>> sort_columns = []
>>>
>>> class_object = class_for_name("app.models", main_model_name)
>>> results = (class_object.objects.filter(**conditions_dict)
...                                .values(*display_columns)
...                                .order_by(*sort_columns)
...            )[:50]
>>>
>>> # *** INSERT HERE ALL CODE THAT WAS ABOVE ***
>>>
>>> print(out)
[{'location': 'AB-Delaware', 'pallet_id': 1}]

It works equally with 'pallet_id' or with 'pallet' in display_columns. Even that "_meta" starts with underscore, it is a documented API.

Hunsaker answered 10/8, 2017 at 20:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.