I've written a custom editable table with columns subclassing from django_tables2.Column
, but keep struggling with rendering a select tag in my custom column. Considering the model:
myapp/models.py
from django.db import models
from myapp.utils.enums import MyModelChoices
class MyModel(models.Model):
bound_model = models.ForeignKey(
SomeOtherModel,
related_name='bound_model'
)
used_as = models.CharField(
max_length=50,
blank=True,
null=True,
choices=MyModelChoices.choices()
)
and my enum in myapp/utils/enums.py:
class MyModelChoices:
__metaclass__ = EnumMeta # Logic irrelevant
First = 'First',
Second = 'Second',
Third = 'Third'
I end up with custom column like this:
import django_tables2 as tables
from django.forms import ChoiceField
class ChoicesColumn(tables.Column):
def __init__(self, choices, attrs=None, **extra):
self.choices = choices
kwargs = {'orderable': False, 'attrs': attrs}
kwargs.update(extra)
super(ChoicesColumn, self).__init__(**kwargs)
def render(self, value, bound_column):
select = ChoiceField(choices=self.choices)
return select.widget.render(
bound_column.name,
self.label_to_value(value)
)
def label_to_value(self, label):
for (v, l) in self.choices:
if l == label:
return v
which is later called in my table class like this:
import django_tables2 as tables
from myapp.models import MyModel
from myapp.tables.utils import ChoicesColumn
class MyTable(tables.Table):
name = tables.Column()
used_as = ChoicesColumn(
choices=lambda record: record.used_as.choices()
)
def render_name(self, record):
return record.bound_model.name
class Meta:
model = MyModel
fields = ('name', 'used_as',)
but still there's rendered just a plain <td></td>
with text instead of select field. What am I doing wrong in this situation? I'm using Python 2.7, Django 1.8 and django-tables2 1.16.0. Thanks in advance for your advice!
UPDATE
I changed my custom column class like this:
class ChoicesColumn(tables.Column):
def __init__(self, attrs=None, **extra):
kwargs = {'orderable': False, 'attrs': attrs}
kwargs.update(extra)
super(ChoicesColumn, self).__init__(**kwargs)
def render(self, value, bound_column):
options = [self.render_option(c) for c in value]
html_template = '''
<select name={}>{}</select>
'''.format(bound_column.name, options)
return mark_safe(html_template)
def render_option(self, choice):
return '<option value={0}>{0}</option>'.format(choice)
and added a render_options
method according to this paragraph in documentation:
class MyTable(tables.Table):
name = tables.Column(accessor='pk')
# With or without accessor it doesn't work neither way
used_as = ChoicesColumn(accessor='used_as')
def render_name(self, record):
return record.bound_model.name
def render_used_as(self, record):
return record.used_as.choices()
class Meta:
model = MyModel,
fields = ('name', 'options',)
but this method isn't even executed on render, what I've spotted while debugging, though the method before it executes when I reload the page and renders data correctly. Is that because name
column uses the library class, and options
column uses custom class inherited from it? If so, what is my subclass missing?
ANOTHER UPDATE
I figured out what was the previous problem with choices, though it didn't solve the problem :( The thing was that I was passing model instance's field used_as
, which was set to None
, thus it would never populate the ChoiceField. So, I rolled back my custom column class to the initial variant, and in my table class instead of
used_as = ChoicesColumn(
choices=lambda record: record.used_as.choices()
)
I imported MyModelChoices
enum and used it instead of model instance
used_as = ChoicesColumn(choices=MyModelChoices.choices())
and now I see the options passing to constructor, though the render
method isn't still called for some mysterious reason =/
LAST UPDATE AS FOR NOW As for the current moment my custom column and table look like this:
class ChoicesColumn(tables.Column):
def __init__(self, choices, attrs=None, **extra)
self.choices = choices
self.choices.insert(0, ('', '------'))
kwargs = {'orderable': False, 'attrs': attrs}
kwargs.update(extra)
super(ChoicesColumn, self).__init__(**kwargs)
def render(self, value, bound_column):
select = forms.ChoiceField(choices=self.choices)
return select.widget.render(bound_column.name, value)
class MyTable(tables.Table):
name = tables.Column(accessor='pk')
used_as = ChoiceColumn(UsedAs.choices(), accessor='used_as')
def render_name(self, record):
return record.bound_model.name
def render_used_as(self, record):
if record.used_as is None:
return ''
return record.used_as
class Meta:
model = MyModel
fields = ('name', 'used_as')
The ChoiceColumn
render method and the corresponding method in table class are never called on rendering stage (unlike the other columns), and I completely give up. Please, be merciful enough either to shoot me or tell me where exactly I'm an idiot :)
So, as I accidentally found out, the problem was in accessor
attribute – when changed from
used_as = ChoiceColumn(UsedAs.choices(), accessor='used_as')
to
used_as = ChoiceColumn(UsedAs.choices(), accessor='pk')
it finally rendered. I don't understand why that happened and would be very grateful if someone explained that to me.