Non-queryset data ordering in django-tables2 - django

The docs say:
Where the table is backed by a model, the database will handle the ordering. Where this is not the case, the Python cmp function is used and the following mechanism is used as a fallback when comparing across different types: ...
But is this possible in a table that is backed by a model, on a custom column? e.g.
class MyModel(models.Model):
x = models.IntegerField()
y = models.IntegerField()
def z(self):
return x+y
class MyTable(tables.Table):
z = tables.Column()
class Meta:
model = MyModel
When I try something like this, the column displays OK, but when I click on the column header to sort, I get this error:
Caught FieldError while rendering: Cannot resolve keyword u'z' into field. Choices are: ...
Apparently this is because z is not found in the database table.
Is there a way around this?

You can't use a queryset if you're ordering on an attribute that doesn't have a database column. You can pass a list to your table though.
Assuming your models.py looks like this:
from django.db import models
class MyModel(models.Model):
def foo(self):
return something_complex()
You could have tables.py that looks like this:
import django_tables2 as tables
from .models import MyModel
class MyModelTable(tables.Table):
foo = tables.Column()
class Meta:
model = MyModel
Then in your views.py:
from django_tables2.config import RequestConfig
from django.core.paginator import InvalidPage
from django.shortcuts import render
def view_my_models(request):
# use a list so django_tables2 sorts in memory
my_models = list(MyModel.objects.all())
my_models_table = MyModelTable(my_models)
RequestConfig(request).configure(my_models_table)
try:
page_number = int(request.GET.get('page'))
except (ValueError, TypeError):
page_number = 1
try:
my_models_table.paginate(page=page_number, per_page=10)
except InvalidPage:
my_models_table.paginate(page=1, per_page=10)
template_vars = {'table': my_models_table}
return render(response, "view_my_models.html", template_vars)
There's also an open ticket discussing this issue.

Related

Reload choices dynamically when using MultipleChoiceFilter

I am trying to construct a MultipleChoiceFilter where the choices are the set of possible dates that exist on a related model (DatedResource).
Here is what I am working with so far...
resource_date = filters.MultipleChoiceFilter(
field_name='dated_resource__date',
choices=[
(d, d.strftime('%Y-%m-%d')) for d in
sorted(resource_models.DatedResource.objects.all().values_list('date', flat=True).distinct())
],
label="Resource Date"
)
When this is displayed in a html view...
This works fine at first, however if I create new DatedResource objects with new distinct date values I need to re-launch my webserver in order for them to get picked up as a valid choice in this filter. I believe this is because the choices list is evaluated once when the webserver starts up, not every time my page loads.
Is there any way to get around this? Maybe through some creative use of a ModelMultipleChoiceFilter?
Thanks!
Edit:
I tried some simple ModelMultipleChoice usage, but hitting some issues.
resource_date = filters.ModelMultipleChoiceFilter(
field_name='dated_resource__date',
queryset=resource_models.DatedResource.objects.all().values_list('date', flat=True).order_by('date').distinct(),
label="Resource Date"
)
The HTML form is showing up just fine, however the choices are not accepted values to the filter. I get "2019-04-03" is not a valid value. validation errors, I am assuming because this filter is expecting datetime.date objects. I thought about using the coerce parameter, however those are not accepted in ModelMultipleChoice filters.
Per dirkgroten's comment, I tried to use what was suggested in the linked question. This ends up being something like
resource_date = filters.ModelMultipleChoiceFilter(
field_name='dated_resource__date',
to_field_name='date',
queryset=resource_models.DatedResource.objects.all(),
label="Resource Date"
)
This also isnt what I want, as the HTML now form is now a) displaying the str representation of each DatedResource, instead of the DatedResource.date field and b) they are not unique (ex if I have two DatedResource objects with the same date, both of their str representations appear in the list. This also isnt sustainable because I have 200k+ DatedResources, and the page hangs when attempting to load them all (as compared to the values_list filter, which is able to pull all distinct dates out in seconds.
One of the easy solutions will be overriding the __init__() method of the filterset class.
from django_filters import filters, filterset
class FooFilter(filterset.FilterSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
try:
self.filters['user'].extra['choices'] = [(d, d.strftime('%Y-%m-%d')) for d in sorted(
resource_models.DatedResource.objects.all().values_list('date', flat=True).distinct())]
except (KeyError, AttributeError):
pass
resource_date = filters.MultipleChoiceFilter(field_name='dated_resource__date', choices=[], label="Resource Date")
NOTE: provide choices=[] in your field definition of filterset class
Results
I tested and verified this solution with following dependencies
1. Python 3.6
2. Django 2.1
3. DRF 3.8.2
4. django-filter 2.0.0
I used following code to reproduce the behaviour
# models.py
from django.db import models
class Musician(models.Model):
name = models.CharField(max_length=50)
def __str__(self):
return f'{self.name}'
class Album(models.Model):
artist = models.ForeignKey(Musician, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
release_date = models.DateField()
def __str__(self):
return f'{self.name} : {self.artist}'
# serializers.py
from rest_framework import serializers
class AlbumSerializer(serializers.ModelSerializer):
artist = serializers.StringRelatedField()
class Meta:
fields = '__all__'
model = Album
# filters.py
from django_filters import rest_framework as filters
class AlbumFilter(filters.FilterSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.filters['release_date'].extra['choices'] = self.get_album_filter_choices()
def get_album_filter_choices(self):
release_date_list = Album.objects.values_list('release_date', flat=True).distinct()
return [(date, date) for date in release_date_list]
release_date = filters.MultipleChoiceFilter(choices=[])
class Meta:
model = Album
fields = ('release_date',)
# views.py
from rest_framework.viewsets import ModelViewSet
from django_filters import rest_framework as filters
class AlbumViewset(ModelViewSet):
serializer_class = AlbumSerializer
queryset = Album.objects.all()
filter_backends = (filters.DjangoFilterBackend,)
filter_class = AlbumFilter
Here I've used the django-filter with DRF.
Now, I populated some data through Django Admin console. After that, the album api become as below,
and I got the release_date as
Then, I added new entry through Django admin -- (Screenshot) and I refresh the DRF API endpoint and the possible choices became as below,
I have looked into your problem and I have following suggestions
The Problem
You have got the problem right. Choices for your MultipleChoiceFilter are calculated statically whenever you run server.Thats why they don't get updated dynamically whenever you insert new instance in DatedResource.
To get it working correctly, you have to provide choices dynamically to MultipleChoiceFilter. I searched in documentation but did not find anything regarding this. So here is my solution.
The solution
You have to extend MultipleChoiceFilter and create your own filter class. I have created this and here it is.
from typing import Callable
from django_filters.conf import settings
import django_filters
class LazyMultipleChoiceFilter(django_filters.MultipleChoiceFilter):
def get_field_choices(self):
choices = self.extra.get('choices', [])
if isinstance(choices, Callable):
choices = choices()
return choices
#property
def field(self):
if not hasattr(self, '_field'):
field_kwargs = self.extra.copy()
if settings.DISABLE_HELP_TEXT:
field_kwargs.pop('help_text', None)
field_kwargs.update(choices=self.get_field_choices())
self._field = self.field_class(label=self.label, **field_kwargs)
return self._field
Now you can use this class as replacement and pass choices as lambda function like this.
resource_date = LazyMultipleChoiceFilter(
field_name='dated_resource__date',
choices=lambda: [
(d, d.strftime('%Y-%m-%d')) for d in
sorted(resource_models.DatedResource.objects.all().values_list('date', flat=True).distinct())
],
label="Resource Date"
)
Whenever instance of filter will be created choices will be updated dynamically. You can also pass choices statically (without lambda function) to this field if want default behavior.

Serialize foreign key object that is AbstractUser

I am serializing a model using the Django REST framework successfully, but would like to add a field from a related model. I have seen other posts describe how to do this using nested serializers, however mine is different because the other model I am trying to access is an AbstractUser class.
I would like to serialize the UserDefinedEquipName field from CustomUser.
models (some fields removed for clarity):
accounts/models.py
from django.contrib.auth.models import AbstractUser
class CustomUser(AbstractUser):
UserDefinedEquipName = models.CharField(max_length=50, default = "Default equip",)
....
builds/models.py
from accounts.models import CustomUser
from django.contrib.auth import get_user_model
class Build(models.Model):
author = models.ForeignKey(get_user_model(),on_delete=models.CASCADE,)
machineName = models.OneToOneField(max_length=50,blank=True,)
....
So my thought is to pass the value into the serializer but can't seem to figure out how to access the value without getting error AttributeError: type object 'Build' has no attribute 'CustomUser'
I have tried:
My serializers.py:
from rest_framework import serializers
from .models import Data, Build, CustomUser
from django.contrib.auth.models import AbstractUser
class buildStatsAPI_serializer(serializers.ModelSerializer):
equipName = Build.CustomUser.UserDefinedEquipName
#also tried:
#equipName = Build.CustomUser__set.all()
class Meta:
fields = ('id','author','machineName','equipName',)
model = Build
Am I missing something small here? Or is there a much better way of doing this. It seems like if this wasn't an AbstractUser class it would be much easier.
EDIT - Added views.py
class buildStatsAPI(generics.ListCreateAPIView):#for build stats JSON
permission_classes = (permissions.IsAuthenticated,)
serializer_class = buildStatsAPI_serializer
def get_queryset(self):
machinesOwned =CustomUser.objects.filter(customerTag=self.request.user.customerTag).filter(isDevice=True)
machineList = []
for machine in machinesOwned:
machineList = machineList + [machine.id]
query = Build.objects.filter(deleted=0, author_id__in=machineList,).values().order_by('pk')
return query
I think you are defining the Serializer improperly. You can't directly reference Model in a serializer. You need to use any kind of fields. For example, if you use SerializerMethodField, you can try like this:
class buildStatsAPI_serializer(serializers.ModelSerializer):
equipName = serializers.SerializerMethodField()
class Meta:
fields = ('id','author','machineName','equipName',)
model = Build
def get_equipName(self, obj):
# here obj is a build model object
return obj.author.UserDefinedEquipName
Update
Please update your get_queryset method so that it returns a queryset like this(I have refactored it a bit):
def get_queryset(self):
query = Build.objects.filter(deleted=0, author__customerTag=self.request.user.customerTag, author__isDevice=True) # removed .values() because it will return a dictionary
return query

Is there a way to always order a table by a specific column in django-tables2?

I'm using django-tables2 to render a table of MyModel. MyModel has a few different categories specified by its category field. I want to be able to overwrite order_by such that the table's primary ordering is always category and anything else selected is just a secondary ordering. Any suggestions on how I might do this?
For anyone else with this issue, I eventually got to an answer:
class PersonTable(tables.Table):
def __init__(self, *args, **kwargs):
def generate_order_func(field_name: str) -> Callable:
# django-tables2 stamps any secondary ordering if you have a custom ordering function on any of your
# ordering fields. This adds custom ordering to every field so that name is always the primary
# ordering
def order_func(qs: QuerySet, descending: bool) -> Tuple[QuerySet, bool]:
# Need custom ordering on deal_state (not alphabetical)
qs = qs.order_by("name", ("-" if descending else "") + field_name)
return qs, True
return order_func
for field in self._meta.fields:
setattr(self, f"order_{field}", generate_order_func(field))
super().__init__(*args, **kwargs)
This overwrites the ordering for each field such that it's ordered by the primary ordering firt.
Late to forum.I haven't tried this below method. But it might help I think. Create a table in tables.py. And add that table to your view as usual. With the added table you may try order_by which is supported in django-tables2.
**tables.py**
import django_tables2 as tables
from .models import Person
class PersonTable(tables.Table):
class Meta:
model = Person
template_name = 'django_tables2/bootstrap.html'
**views.py**
from django.shortcuts import render
from django_tables2 import RequestConfig
from .tables import PersonTable
def people_listing(request):
config = RequestConfig(request)
table = PersonTable(Person.objects.all())
table.order_by = 'name'
return render(request, 'data/person.html', {'table': table})

DRF serializer filtering

I have a serializer that gives me everything fine.
ModelClassASerializer((serializers.ModelSerializer)):
.....
status = serializers.SerializerMethodField()
def get_status(self, obj):
....
status = ModelB.objects.get(id=obj.id).status
....
return status
class Meta:
model = ModelClassA
fields = (...)
But if I want to make a filtering based on that status, I can't. I am using django_filters.rest_framework.FilterSet for the filtering. There is no relation between models.
What is the best way to do that filtering?
It looks like objects in ModelA share the same IDs as those in ModelB. If that's the case, you can use a subquery to match on IDs. If the IDs do not correspond with each other, then this query will be nonsensical. You want to create the following queryset:
from django.db.models import Subquery
from myapp.models import ModelA, ModelB
pks = ModelB.objects.filter(status='foo').values('pk')
ModelA.objects.filter(pk__in=Subquery(pks))
To create the above will django-filter, you'll need to use the method argument on a filter.
from django_filters import rest_framework as filters
class ModelAFilter(filters.FilterSet):
status = filters.ChoiceFilter(choices=(('foo', 'Foo'), ...), method='filter_status')
class Meta:
model = ModelA
fields = []
def filter_status(self, queryset, name, value):
pks = ModelB.objects.filter(status=value).values('pk')
return queryset.filter(pk__in=Subquery(pks))

in django admin, can we have a multiple select based on choices

http://docs.djangoproject.com/en/dev/ref/models/fields/#choices
i've read through the documentation and this implies using a database table for dynamic data, however it states
choices is meant for static data that doesn't change much, if ever.
so what if i want to use choices, but have it select multiple because the data i'm using is quite static, e.g days of the week.
is there anyway to achieve this without a database table?
ChoiceField is not really suitable for multiple choices, instead I would use a ManyToManyField. Ignore the fact that Choices can be used instead of ForeignKey for static data for now. If it turns out to be a performance issue, there are ways to represent this differently (one being a binary mask approach), but they require way more work.
This worked for me:
1) create a Form class and set an attribute to provide your static choices to a MultipleChoiceField
from django import forms
from myapp.models import MyModel, MYCHOICES
class MyForm(forms.ModelForm):
myfield = forms.MultipleChoiceField(choices=MYCHOICES, widget=forms.SelectMultiple)
class Meta:
model = MyModel
2) then, if you're using the admin interface, set the form attribute in your admin class so tit will use your customized form
from myapp.models import MyModel
from myapp.forms import MyForm
from django.contrib import admin
class MyAdmin(admin.ModelAdmin):
form = MyForm
admin.site.register(MyModel, MyAdmin)
Try following configuration. In models.py
class MyModel(models.Model):
my_choices = models.TextField(help_text="It's a good manners to write it")
in forms.py
CHOICES = ((1,1), (2,2))
class MyForm(forms.ModelForm):
my_choices = forms.MultipleChoiceField(choices=CHOICES)
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
# maybe you can set initial with self.fields['my_choices'].initial = initial
# but it doesn't work wity dynamic choices
obj = kwargs.get('instance')
if obj:
initial = [i for i in obj.my_choices.split(',')]
self.initial['my_choices'] = initial
def clean_lead_fields(self):
return ','.join(self.cleaned_data.get('my_choices', []))
in admin.py
#admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
form = MyModelForm