The Django admin docs says that it is possible to specify a callable as a value that can be used in list_display. If I need to pass some extra context to the function via function arguments, what's the best way to accomplish that?
In pseudo code, what I'd like to do is something like:
App realestate:
models.py:
class A(models.Model):
raw = models.TextField()
admin.py:
from utils import processing
list_display = [processing('realestate app result', True)]
App party:
models.py:
class Person(models.Model):
raw = models.TextField()
admin.py:
from utils import processing
list_display = [processing('party app result', False)]
utils.py:
def processing(obj, short_description, allow_tags=False):
def process(obj):
# do something
pass
process.short_description = short_description
process.allow_tags = allow_tags
return process(obj)
You should not specify the short_description and allow_tags inside the function itself.
define processing like this:
utils.py:
def processing(obj):
#do something
pass
on each ModelAdmin class, do:
class RealestateAdmin(...):
list_display = [processing,]
def processing(obj):
return utils.processing(obj)
processing.short_description = 'realestate app result'
processing.allow_tags = True
class PartyAdmin(...):
list_display = [processing,]
def processing(obj):
return utils.processing(obj)
processing.short_description = 'party app result'
processing.allow_tags = False
So you have one place (utils.py) which contains the logic.
And every class defines the properties to display in the table.
Related
I can't import Django-taggit tags using Django-import-export.
This error is when the value is entered.
Line number: 1 - invalid literal for int() with base 10: 'def'
Also, this error is when the value is blank.
Line number: 2 - Cannot add <QuerySet []> (<class 'django.db.models.query.QuerySet'>). Expected <class 'django.db.models.base.ModelBase'> or str.
I also posted question in This issue.
xlsx table there is an id column too.
models.py
from django.db import models
from django.urls import reverse
from taggit.managers import TaggableManager
class KnowHow(models.Model):
author = models.ForeignKey('auth.User',on_delete=models.CASCADE)
title = models.CharField(max_length=200)
text = models.TextField(blank=True)
file = models.FileField(blank=True,upload_to='explicit_knowhows')
free_tags = TaggableManager(blank=True)
def __str__(self):
return self.title
admin.py
from django.contrib import admin
from import_export import resources
from import_export import fields
from import_export.admin import ImportExportModelAdmin
from .models import KnowHow
# Register your models here.
class KnowHowResource(resources.ModelResource):
class Meta:
model = KnowHow
import_id_fields = ['id']
#admin.register(KnowHow)
class knowHowAdmin(ImportExportModelAdmin):
resource_class = KnowHowResource
My solution:
Custom widget:
from import_export import fields
from import_export import widgets
from taggit.forms import TagField
from taggit.models import Tag
class TagWidget(widgets.ManyToManyWidget):
def render(self, value, obj=None):
return self.separator.join(
[obj.name for obj in value.all()]
)
def clean(self, value, row=None, *args, **kwargs):
values = TagField().clean(value)
return [
Tag.objects.get_or_create(name=tag)[0]
for tag in values
]
Then we have to override field as well:
class TagFieldImport(fields.Field):
def save(self, obj, data, is_m2m=False):
# This method is overridden because originally code
# getattr(obj, attrs[-1]).set(cleaned, clean=True) doesn't unpack cleaned value
if not self.readonly:
attrs = self.attribute.split('__')
for attr in attrs[:-1]:
obj = getattr(obj, attr, None)
cleaned = self.clean(data)
if cleaned is not None or self.saves_null_values:
if not is_m2m:
setattr(obj, attrs[-1], cleaned)
else:
# Change only here
getattr(obj, attrs[-1]).set(*cleaned, clean=True)
And then to use in the resource like that:
tags = cure_widgets.TagFieldImport(
attribute="tags",
column_name="tags",
widget=cure_widgets.TagWidget(Tag, separator=", ")
)
Another possible solution / workaround I use.
Define a helper model text-field to store the comma-separated tag list. Let's call it "tags_char". Then override the save method of the model to convert the comma-separated list to Tag objects.
# models.py
from django.db import models
from taggit.managers import TaggableManager
class Book(models.Model):
tags_char = models.TextField(blank=True)
tags = TaggableManager(blank=True)
def save(self, *args, **kwargs):
if ',' in self.tags_char and self.pk:
for tag in self.tags_char.split(','):
self.tags.add(tag)
super(Book, self).save(*args, **kwargs)
The final step is to adjust the ModelResource. We exclude tags from import and instead import to tags_char from the file. Knowing that the conversion will happen afterwards once the objects are saved.
#admin.py
class BookResource(resources.ModelResource):
class Meta:
model = Book
exclude = ('id', 'tags')
Unfortunately there's one problem with this workaround. Beacuse of the way django import-export works the save() method is called prior to objects being assigned an id (pk). And a tag can not be added if an id does not eixst. This is the reason for checking if self.pk exists in the save method.
if ',' in self.tags_char and self.pk
There are three alternatives to get around the issue.
If you're going to complement the data or review it before publishing them to the website. You will need to re-save them manually => Problems solved (this is the case for me)
Just import the file two times. The second time around the id's will exists.
Provide an id in the file to import.
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.
There is an easier way:
If you have a Enum column (say used_as), you can change the renderer so that it displays the value (instead of the name). Place this in the Table definition (in class MyTable(tables.Table) ).
def render_used_as(self,value):
v = value.split(".")[1]
members = MyModelChoices.__members__
return (members[v].value)
Note that I was using a bit of a different syntax for the Enum
from enum import Enum
Class MyModelChoices(Enum):
First = 'First'
Second = 'Second'
Third = 'Third'
Note: render_used_as is render_%s with %s = variable name
There are games entity, each of them could have 1 or more platforms. Also each game could have 1 or more links to related games (with their own platforms). Here it looks like in models.py:
class Game(TimeStampedModel):
gid = models.CharField(max_length=38, blank=True, null=True)
name = models.CharField(max_length=512)
platforms = models.ManyToManyField(
Platform, blank=True, null=True)
...
#here is the self-referencing m2m field
related_games = models.ManyToManyField(
"self", related_name="related", blank=True)
And this model are served with this code in admin.py:
#admin.register(Game)
class GameAdmin(AdminImageMixin, reversion.VersionAdmin):
list_display = ("created", "name", "get_platforms"... )
list_filter = ("platforms", "year",)
#I'm interested in changing the field below
filter_horizontal = ("related_games",)
formfield_overrides = {
models.ManyToManyField: {"widget": CheckboxSelectMultiple},
}
def get_platforms(self, obj):
return ", ".join([p.name for p in obj.platforms.all()])
I need to extend filter_horizontal = ("related_games",) part of admin.py - to add a platform information of each game in related games widget. It should look like (game name and platforms list): "Virtual Fighter (PS4, PSP, PS3)".
The application uses Django 1.7 and Python 2.7
Thank you for your attention.
By default, what is shown for each item in a filter_horizontal is based on the object's __str__ or __unicode__ method, so you could try something like the following:
class Game(TimeStampedModel):
# field definitions
# ...
def __unicode__(self):
return '{0} ({1})'.format(
self.name,
(', '.join(self.platforms.all()) if self.platforms.exists()
else 'none')
)
This will make each game show in the list (and everywhere else) as "Name (Platforms)", for example "Crash Bandicoot (PS1, PS2)" or "Battlefield (none)" if it doesn't have any platforms
Alternatively, if you don't want to change the __unicode__ method of your model, you'll need to set your ModelAdmin to use a custom ModelForm, specifying that the related_games field should use a custom ModelMultipleChoiceField with a custom FilteredSelectMultiple widget, in which you will need to override the render_options method. The following classes should be in their respective separate files, but it would look something like:
# admin.py
class GameAdmin(AdminImageMixin, reversion.VersionAdmin):
# ...
form = GameForm
# ...
# forms.py
from django import forms
class GameForm(forms.ModelForm):
related_games = RelatedGamesField()
class Meta:
fields = (
'gid',
'name',
'platforms',
'related_games',
)
# fields.py
from django.forms.models import ModelMultipleChoiceField
class RelatedGamesField(ModelMultipleChoiceField):
widget = RelatedGamesWidget()
# widgets.py
from django.contrib.admin.widgets import FilteredSelectMultiple
class RelatedGamesWidget(FilteredSelectMultiple):
def render_options(self, choices, selected_choices):
# slightly modified from Django source code
selected_choices = set(force_text(v) for v in selected_choices)
output = []
for option_value, option_label in chain(self.choices, choices):
if isinstance(option_label, (list, tuple)):
output.append(format_html(
'<optgroup label="{0}">',
# however you want to have the related games show up, eg.,
'{0} ({1})'.format(
option_value.name,
(', '.join(option_value.platforms.all())
if option_value.platforms.exists() else 'none')
)
))
for option in option_label:
output.append(self.render_option(selected_choices, *option))
output.append('</optgroup>')
else:
output.append(self.render_option(selected_choices, option_value, option_label))
return '\n'.join(output)
Let's say I have model like this:
class Article(models.Model):
...
def get_route(self):
...
return data
When I want to display value returned by get_route() in admin panel I have to create custom method like this:
def list_display_get_route(self, obj):
return obj.get_route()
list_display = ('list_display_get_route', ...)
I've recently discovered that if I decorate get_route() with #property there's no need to use list_display_get_route() and the following will work:
models.py
#property
def get_route(self):
...
return data
admin.py
list_display = ('get_route', ...)
Is this correct approach? Are there any drawbacks of this solution?
Using get_route in list_display should work even if you don't make it a property.
class Article(models.Model):
def get_route(self):
...
return data
class ArticleAdmin(admin.ModelAdmin):
list_display = ('get_route', ...)
See the list_display docs for the list of value types supported. The fourth example, "a string representing an attribute on the model", is equivalent to your get_route method.
Using the property decorator is fine if you prefer, but in this case it would make more sense to name it route.
#property
def route(self):
...
return data
class ArticleAdmin(admin.ModelAdmin):
list_display = ('route', ...)
I'm using Django forms and need to create a list box.
What would be the equivalent of listbox in Django form fields?
I checked the documentation #
https://docs.djangoproject.com/en/dev/ref/forms/fields/#modelchoicefield
but unable to find it.
Here is my code snippet,
Models.py
class Volunteer(models.Model):
NO_OF_HRS = (('1','1')
('2','2'))
datecreated = models.DateTimeField()
volposition = models.CharField(max_length=300)
roledesc = models.CharField(max_length=300)
Duration = models.CharField(choices=NO_OF_HRS,max_length=1)**
forms.py
class VolunteerForm(forms.ModelForm)
datecreated = forms.DateField(label=u'Creation Date')
volposition = forms.CharField(label=u'Position Name', max_length=300)
roledesc = forms.roledesc(label=u'Role description',max_length=5000)
Duration = forms.CharField(widget=forms.select(choices=NO_OF_HRS),max_length=2)
When I try to run, I get the following error,
NO_OF_HRS is not defined
Your NO_OF_HRS tuple is defined inside the model and not available to the form. It has to be imported in forms.py just like any other Python object. Try moving the tuple outside the model definition and import in your forms.py like this:
models.py
NO_OF_HRS = (('1','1')
('2','2'))
class Volunteer(models.Model):
# ...
duration = models.CharField(choices=NO_OF_HRS, max_length=1)
forms.py
from path.to.models import NO_OF_HRS
class VolunteerForm(forms.Form):
# ...
duration = forms.CharField(widget=forms.Select(choices=NO_OF_HRS), max_length=1)
It also looks like you want to use a ModelForm. In this case you don't need to add any field definitions to your VolunteerForm, simply set your model in the inner Meta class.
forms.py
from path.to.models Volunteer
class VolunteerForm(forms.ModelForm):
class Meta:
model = Volunteer