Model field links as foreign_key to another model which has big amount of entries.
I decided to replace default select for foreign_keys with simple link.
And it works grate besides the fact that field becomes hidden!
What should I do to avoid that?
admin.py
class SeriesAdmin(ModelLinkAdminFields, admin.ModelAdmin):
modellink = ['video',]
wdiget_file.py
class ModelLinkWidget(forms.HiddenInput):
def __init__(self, admin_site, original_object):
self.admin_site = admin_site
self.original_object = original_object
super(ModelLinkWidget,self).__init__()
def render(self, name, value, attrs=None):
if self.original_object is not None:
change_url = urlresolvers.reverse('admin:%s_%s_change' %
(type(self.original_object)._meta.app_label,
type(self.original_object)._meta.object_name.lower()),
args=(self.original_object.id,))
return mark_safe('<a id="%s" name="{name}" href="%s">%s</a>' %
(attrs['id'], change_url , escape(self.original_object)))
else:
return None
class ModelLinkAdminFields(object):
def get_form(self, request, obj=None, **kwargs):
form = super(ModelLinkAdminFields, self).get_form(request, obj, **kwargs)
if hasattr(self, 'modellink'):
for field_name in self.modellink:
if field_name in form.base_fields:
form.base_fields[field_name].widget = ModelLinkWidget(self.admin_site, getattr(obj, field_name, ''))
return form
Your widget overrides forms.HiddenInput
You should use the correct widget, I presume this should be Select but there are other options available
Related
I have a model with choices list (models.py):
class Product(models.Model):
...
UNITS_L = 1
UNITS_SL = 2
UNITS_XL = 3
PRODUCT_SIZE_CHOICES = (
(UNITS_L, _('L')),
(UNITS_SL, _('SL')),
(UNITS_XL), _('XL'),
)
product_size = models.IntegerField(choices=PRODUCT_SIZE_CHOICES)
...
Also I added a new class for exporting needed fields(admin.py):
from import_export import resources, fields
...
Class ProductReport(resources.ModelResource):
product_size = fields.Field()
class Meta:
model = Product
#I want to do a proper function to render a PRODUCT_SIZE_CHOICES(product_size)
def dehydrate_size_units(self, product):
return '%s' % (product.PRODUCT_SIZE_CHOICES[product_size])
fields = ('product_name', 'product_size')
Class ProductAdmin(ExportMixin, admin.ModelAdmin):
resource_class = ProductReport
But this is not working. How can I get a named value of PRODUCT_SIZE_CHOICES in export by Django import-export ?
You can use 'get_FOO_display' to achieve this in the Django Admin:
class ProductReportResource(resources.ModelResource):
product_size = fields.Field(
attribute='get_product_size_display',
column_name=_(u'Product Size')
)
In my case I was trying to get the display from a foreign key choice field, like:
user__gender
After unsuccessfully trying the accepted answer and the other answer by Waket, I found this thread here:
https://github.com/django-import-export/django-import-export/issues/525
From where I tried a couple of options, and the one that finally worked for me is this:
Create the widget somewhere
from import_export.widgets import Widget
class ChoicesWidget(Widget):
"""
Widget that uses choice display values in place of database values
"""
def __init__(self, choices, *args, **kwargs):
"""
Creates a self.choices dict with a key, display value, and value,
db value, e.g. {'Chocolate': 'CHOC'}
"""
self.choices = dict(choices)
self.revert_choices = dict((v, k) for k, v in self.choices.items())
def clean(self, value, row=None, *args, **kwargs):
"""Returns the db value given the display value"""
return self.revert_choices.get(value, value) if value else None
def render(self, value, obj=None):
"""Returns the display value given the db value"""
return self.choices.get(value, '')
In your model resource declare the field using the widget and passing the choices to it, like this:
user__gender = Field(
widget=ChoicesWidget(settings.GENDER_CHOICES),
attribute='user__gender',
column_name="Gènere",
)
Another solution:
class BaseModelResource(resources.ModelResource):
def export_field(self, field, obj):
field_name = self.get_field_name(field)
func_name = 'get_{}_display'.format(field_name)
if hasattr(obj, func_name):
return getattr(obj, func_name)
return super().export_field(field, obj)
class ProductReportResource(BaseModelResource):
...
I wanted a multiState clickbox. So I spend some free time on a nice Django solution that makes it:
class MultiStateChoiceInput(forms.widgets.ChoiceInput):
input_type = 'radio'
def __init__(self, name, value, attrs, choice, index, label_id):
# Override to use the label_id which is upped with 1
if 'id' in attrs:
self.label_id = attrs['id']+ "_%d" % label_id
super(MultiStateChoiceInput, self).__init__(name, value, attrs, choice, index)
self.value = force_text(self.value)
#property
def id_for_label(self):
return self.label_id
def render(self, name=None, value=None, attrs=None, choices=()):
if self.id_for_label:
label_for = format_html(' for="{}"', self.id_for_label)
else:
label_for = ''
attrs = dict(self.attrs, **attrs) if attrs else self.attrs
return format_html(
'{} <label{}>{}</label>', self.tag(attrs), label_for, self.choice_label
)
class MultiStateRenderer(forms.widgets.ChoiceFieldRenderer):
choice_input_class = MultiStateChoiceInput
outer_html = '<span class="cyclestate">{content}</span>'
inner_html = '{choice_value}{sub_widgets}'
def render(self):
"""
Outputs a <ul> for this set of choice fields.
If an id was given to the field, it is applied to the <ul> (each
item in the list will get an id of `$id_$i`).
# upgraded with the label_id
"""
id_ = self.attrs.get('id')
output = []
for i, choice in enumerate(self.choices):
choice_value, choice_label = choice
if isinstance(choice_label, (tuple, list)):
attrs_plus = self.attrs.copy()
if id_:
attrs_plus['id'] += '_{}'.format(i)
sub_ul_renderer = self.__class__(
name=self.name,
value=self.value,
attrs=attrs_plus,
choices=choice_label,
label_id = (i+1) % (len(self.choices)) # label_id is next one
)
sub_ul_renderer.choice_input_class = self.choice_input_class
output.append(format_html(self.inner_html, choice_value=choice_value,
sub_widgets=sub_ul_renderer.render()))
else:
w = self.choice_input_class(self.name, self.value,
self.attrs.copy(), choice, i, label_id = (i+1) % (len(self.choices))) # label_id is next one
output.append(format_html(self.inner_html,
choice_value=force_text(w), sub_widgets=''))
return format_html(self.outer_html,
id_attr=format_html(' id="{}"', id_) if id_ else '',
content=mark_safe('\n'.join(output)))
class MultiStateSelectWidget(forms.widgets.RendererMixin, forms.widgets.Select):
''' This widget enables multistate clickable toggles
Requires some css as well (see .cyclestate)
'''
renderer = MultiStateRenderer
This creates a form like is explained here https://stackoverflow.com/a/33455783/3849359 and where a click toggles the next state until it reached the and and then continues at the beginning.
The form is called in my view like:
SomeFormSet= modelformset_factory(myModel, form=myModelForm, extra=0)
SomeFormSet.form = staticmethod(curry(myModelForm, somevariable=somevariable))
formset = SomeFormSet(request.POST or None, queryset=somequeryset)
And forms.py is:
class myModelForm(forms.ModelForm):
CHOICES = (
(0, _('a')),
(1, _('b')),
(2, _('c')),
(3, _('d')),
)
field = forms.IntegerField(widget=MultiStateSelectWidget(choices=CHOICES))
class Meta:
model = MyModal
fields = ('field',)
widgets = {'id': forms.HiddenInput(),
}
def __init__(self, *args, **kwargs):
self.variable= kwargs.pop('variable')
super(myModelForm, self).__init__(*args, **kwargs)
for field in myModelForm.fields:
if self.instance.pk:
if not getattr(self.instance, field):
self.initial[field]= 0
else:
self.initial[field]= 1
if anothercondition:
self.initial[field] = 3
else:
self.initial[field] = 2
I thought it worked very well. And the clicking and saving does work wel (I have a custom save method). Except when the form field has a value of 2 or 3, then it suddenly failes with the error message: 'field' should be a whole number.
If anyone could help that would be great, as I'm out of ideas!
EDIT: Just in case... I have checked the POST and it is great. The only problem is that Django somewhere in parsing the POST loses the value completely (it becomes None) if the value is a 2 and I have no idea why.
EDIT2: It seems that the Django ModelForm does also do model validation. And the model is a BooleanField, which is the reason why it fails. If anyone knows a good way to override it, that would be nice!
#edgarzamora Your comment is not the answer, but it is close!
I removed the 'field' from the Form class Meta, so it looked like:
class Meta:
model = MyModal
fields = ('',)
widgets = {'id': forms.HiddenInput(),
}
And now everything works, because I have my custom save method... So stupid, it costed me hours! Thanks!
I'm trying to override render_option method present inside Select Widget class from my forms.py file. So I have added the method with the same name inside the corresponding Model form class. But it won't work (this method fails to override). My forms.py file looks like,
class CustomSelectMultiple(Select):
allow_multiple_selected = True
def render_option(self, selected_choices, option_value, option_label):
print 'Inside custom render_option\n\n'
if option_value is None:
option_value = ''
option_value = force_text(option_value)
if option_value in selected_choices:
selected_html = mark_safe(' selected="selected"')
if not self.allow_multiple_selected:
# Only allow for a single selection.
selected_choices.remove(option_value)
else:
selected_html = ''
return format_html('<option value="{}" data-img-src="www.foo.com" {}>{}</option>',
option_value,
selected_html,
force_text(option_label))
def render_options(self, choices, selected_choices):
print 'Inside custom render_options\n\n'
print self
print choices
# Normalize to strings.
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="{}">', force_text(option_value)))
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))
#print output
return '\n'.join(output)
def render(self, name, value, attrs=None, choices=()):
print 'Inside custom render\n\n'
if value is None:
value = []
final_attrs = self.build_attrs(attrs, name=name)
output = [format_html('<select multiple="multiple"{}>', flatatt(final_attrs))]
options = self.render_options(choices, value)
if options:
output.append(options)
output.append('</select>')
return mark_safe('\n'.join(output))
def value_from_datadict(self, data, files, name):
if isinstance(data, MultiValueDict):
return data.getlist(name)
return data.get(name)
class GuideUpdateForm(ModelForm):
def __init__(self, *args, **kwargs):
super(GuideUpdateForm, self).__init__(*args, **kwargs)
self.fields['date_modified'].widget = HiddenInput()
self.fields['point_of_interest'].widget = CustomSelectMultiple()
class Meta:
fields = ('name', 'image', 'point_of_interest', 'date_modified', )
model = Guide
I also tried changing my Meta class like,
class Meta:
fields = ('name', 'image', 'point_of_interest', 'date_modified', )
model = Guide
widgets = {
'point_of_interest': SelectMultiple(attrs={'data-img-src': 'www.foo.com'}),
}
But it add's the attribute data-img-src only to the select tag but not to all the option tags present inside the select tag.
Note that SelectMultiple class invokes the renderoptions method of Select class which further invokes the renderoption method which don't have attrs=None keyword argument.
Judging off your own solution it looks like you may have been looking for a ModelChoiceField
self.fields['point_of_interest'] = forms.ModelChoiceField(widget=CustomSelectMultiple(),
queryset=poi.objects.all())
The queryset parameter consists of "A QuerySet of model objects from which the choices for the field will be derived, and which will be used to validate the user’s selection."
does it create a list of tuples of ids, names? Because I want the option tag to look like option value="id">name</option>
I'm pretty sure the default is id, __str__ where __str__ is the string representation of the model. If you wanted this to be specific to the name then you could override this field and set label_from_instance
class MyModelChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
return obj.name
I managed to solve this problem by passing db values to choices kwargs.
from models import poi
class GuideUpdateForm(ModelForm):
def __init__(self, *args, **kwargs):
super(GuideUpdateForm, self).__init__(*args, **kwargs)
self.fields['date_modified'].widget = HiddenInput()
self.fields['point_of_interest'] = forms.ChoiceField(widget=CustomSelectMultiple(), choices=[(i.id,i.name) for i in poi.objects.all()])
I need help with the following problem:
Using the Django admin, I would like to hide some fields in inline depending on whether the object exists.
Example equivalent to admin.ModelAdmin:
class ClassAdmin(admin.ModelAdmin):
...
def get_form(self, request, obj=None, **kwargs):
# if inline has not been saved
if obj is None:
self.fieldsets[0][1]['fields'] = tuple(x for x in self.fieldsets[0][2]['fields'] if (x!='field1'))
else:
self.inlines = self.inlines + [ClassInline,]
if obj.field1 == 'N':
self.fieldsets[2][7]['fields'] = tuple(x for x in self.fieldsets[2][8]['fields'] if (x!='field10'))
return super(ClassAdmin, self).get_form(request, obj, **kwargs)
How can I make it equivalent to an inline?
class ClassInline(admin.StackedInline):
# if obj:
# display filed1, field2
# else:
# display filed3, field4
I tried hard and not found something to help me solve the problem. Some topics I found:
Here, Here and Here.
Can someone show an example of code that can do the job?
InlineModelAdmin.get_formset() is called with the current object (the current parent object I mean) as param, and builds the list of fields for the inline's form (actually for the call to inlineformset_factory()) by calling on self.get_fieldsets(), passing the current (parent) object. So overriding InlineModelAdmin.get_formset() should do:
class MyInlineAdmin(admin.StackedInline):
def get_fieldsets(self, request, obj):
fields = super(MyInlineAdmin, self).get_fieldsets(request, obj):
if obj and obj.pk:
return do_something_with(fields)
else :
return do_something_else_with(fields)
Now you say you "tried hard" and did "not found something" - but did you at least "tried" to just have a look at the source code ? It took me a couple minutes to figure out the call chain and args...
I solved the problem of a not very clean way, but it works for me.
For anyone with a similar problem and need an example ... See the code below.
in admin.py
class MyInline(admin.StackedInline):
form = MyForm
model = MyModel
fields = ('field1', 'field2', 'field3', 'field4', 'fied5', 'field6')
list_display = ('field2', 'field3', 'field4', 'fied5', 'field6',)
fieldsets = (
(None, {
"fields" : ("field1",)
}),
("Details", {
"fields" : ("field2", "field3", 'field4', 'posicao', 'venda')
})
)
class MyAddInline(MyInline):
"""Inline displayed if there are no objects"""
fieldsets = ((None, {
"fields" : ("field1",)
}),)
class ClassXAdmin(admin.ModelAdmin):
model = MyOtherModel
...
def get_form(self, request, obj=None, **kwargs):
if obj is None:
...
else:
status = MyModel.objects.filter(fk=obj.pk).exists()
if status:
self.inlines = self.inlines + [MyInline,]
else:
self.inlines = self.inlines + [MyAddInline,]
return super(ClassXAdmin, self).get_form(request, obj, **kwargs)
I have the following form:
class TutorForm(SignupForm):
subjects = forms.ModelMultipleChoiceField(queryset=Subject.objects.all(),
widget=forms.CheckboxSelectMultiple())
I have a child form called TutorUpdateFormthat inherits from TutorForm and it sets initial values in init method.
self.fields['subjects'].initial = current_user.subjects.all()
In my template however the values are not checked (in views the values are there, so setting initial values works). How can I enforce checked inputs in the template?
EDIT (init code)
def __init__(self, *args, **kwargs):
current_user = None
try:
current_user = kwargs.pop('user')
except Exception:
pass
super(TutorUpdateForm, self).__init__(*args, **kwargs)
for field in _update_exclude:
self.fields.pop(field)
if current_user:
self.fields['subjects'].initial = current_user.subjects.all()
You should pass the initial values into the call to super, you can also set a default value for dict.pop rather than using try/except
def __init__(self, *args, **kwargs):
current_user = kwargs.pop('user', None)
initial = kwargs.get('initial', {})
if current_user:
initial.update({'subjects': current_user.subjects.all()})
kwargs['initial'] = initial
super(TutorUpdateForm, self).__init__(*args, **kwargs)
for field in _update_exclude:
self.fields.pop(field)
Here is a link to the documentation on dynamic initial values for forms