Django - Admin - Inline - 'extra' value based on some condition - django

Is it possible to dynamically set the 'extra' option in the Django Admin Inline?
For example, If Student class have Address class as Inline.
If there is no Address inline's associated with Student, then extra =1.
If there is any Address inline's associated with Student, then extra =0.

Just simply override the get_extra method. The following example set extra to 0 for the add view and 10 for the edit view.
class MyInline(admin.TabularInline):
model = MyModel
def get_extra(self, request, obj=None, **kwargs):
return 0 if obj else 10

You can just leverage inheritance..
// based on some condition
kwargs['extra'] = something
.........
return super(*******Inline, self).get_formset(request, obj, **kwargs) // 'defaults.update(kwargs)' takes care of the dynamic overriding
The get_formset method from my project :
def get_formset(self, request, obj=None, **kwargs):
## Put in your condition here and assign extra accordingly
if obj is None:
return super(ImageInline, self).get_formset(request, obj, **kwargs)
current_topic = TopicPage.objects.get(pk = obj.id)
topic_images = ThruImage.objects.filter(topic = current_topic)
kwargs['extra'] = 0
if len(topic_images) <= 3:
kwargs['extra'] = 3 - len(topic_images)
return super(ImageInline, self).get_formset(request, obj, **kwargs)
This is of course, useful only for simple conditionals based off the parent model object ..

Not sure if it would work and I am not too familiar with inlines and this extra attribute, but you could subclass django.contrib.admin.InlineModelAdmin and replace the InlineModelAdmin.extra attribute with a python property:
from django.contrib import admin
from myproject.myapp.models import MyInlineModel
class DynamicExtraInlineModelAdmin(admin.InlineModelAdmin):
#property
def extra():
return 1 if some_logic else 0
admin.site.register(MyInlineModel, DynamicExtraInlineModelAdmin)

You just monkey patch django's (1.3.1) source code as follows:
First add the following code to your app:
from django.forms.models import inlineformset_factory
from django.contrib.admin.util import flatten_fieldsets
from django.utils.functional import curry
from django.contrib.admin.options import InlineModelAdmin
class MyInlineModelAdmin(InlineModelAdmin):
#extra = 1
def get_formset(self, request, obj=None, **kwargs):
"""Returns a BaseInlineFormSet class for use in admin add/change views."""
if self.declared_fieldsets:
fields = flatten_fieldsets(self.declared_fieldsets)
else:
fields = None
if self.exclude is None:
exclude = []
else:
exclude = list(self.exclude)
exclude.extend(kwargs.get("exclude", []))
exclude.extend(self.get_readonly_fields(request, obj))
# if exclude is an empty list we use None, since that's the actual
# default
exclude = exclude or None
if obj and hasattr(obj, 'id'): # <<=======================================
_extra = 0
else:
_extra = self.extra
defaults = {
"form": self.form,
"formset": self.formset,
"fk_name": self.fk_name,
"fields": fields,
"exclude": exclude,
"formfield_callback": curry(self.formfield_for_dbfield, request=request),
"extra": _extra,
"max_num": self.max_num,
"can_delete": self.can_delete,
}
defaults.update(kwargs)
return inlineformset_factory(self.parent_model, self.model, **defaults)
class MyTabularInline(MyInlineModelAdmin):
template = 'admin/edit_inline/tabular.html'
and assuming your models are something like:
class ContainerModel(models.Model):
pass #etc...
class ListModel(models.Model):
pass #etc...
then change your admins to:
class ListModelInline(MyTabularInline): # <<=================================
model = MyModel
class ContainerModelAdmin(admin.ModelAdmin):
inlines = (ListModelInline,)
admin.site.register(ContainerModel, ContainerModelAdmin)
#etc...

Related

Django rest framework add field in serializer for using with post method only

am trying to build a serializer using HyperlinkedModelSerializer, yet I have a scenario where I want to add a field which does not exist in the model but I will require the value for validating the transaction, I found the below snippet
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from rest_framework import serializers
import logging
# initiate logger
logging.getLogger(__name__)
class PostHyperlinkedModelSerializerOptions(serializers.HyperlinkedModelSerializerOptions):
"""
Options for PostHyperlinkedModelSerializer
"""
def __init__(self, meta):
super(PostHyperlinkedModelSerializerOptions, self).__init__(meta)
self.postonly_fields = getattr(meta, 'postonly_fields', ())
class PostHyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
_options_class = PostHyperlinkedModelSerializerOptions
def to_native(self, obj):
"""
Serialize objects -> primitives.
"""
ret = self._dict_class()
ret.fields = {}
for field_name, field in self.fields.items():
# Ignore all postonly_fields fron serialization
if field_name in self.opts.postonly_fields:
continue
field.initialize(parent=self, field_name=field_name)
key = self.get_field_key(field_name)
value = field.field_to_native(obj, field_name)
ret[key] = value
ret.fields[key] = field
return ret
def restore_object(self, attrs, instance=None):
model_attrs, post_attrs = {}, {}
for attr, value in attrs.iteritems():
if attr in self.opts.postonly_fields:
post_attrs[attr] = value
else:
model_attrs[attr] = value
obj = super(PostHyperlinkedModelSerializer,
self).restore_object(model_attrs, instance)
# Method to process ignored postonly_fields
self.process_postonly_fields(obj, post_attrs)
return obj
def process_postonly_fields(self, obj, post_attrs):
"""
Placeholder method for processing data sent in POST.
"""
pass
class PurchaseSerializer(PostHyperlinkedModelSerializer):
""" PurchaseSerializer
"""
currency = serializers.Field(source='currency_used')
class Meta:
model = DiwanyaProduct
postonly_fields = ['currency', ]
Am using the above class, PostHyperlinkedModelSerializer, but for some reason the above is causing a problem with the browsable api interface for rest framework. Field labels are disappearing, plus the new field "currency" is not showing in the form (see screenshot below for reference). Any one can help on that?
Whoever wrote the code probably didn't need browsable api (normal requests will work fine).
In order to fix the api change to_native to this:
def to_native(self, obj):
"""
Serialize objects -> primitives.
"""
ret = self._dict_class()
ret.fields = self._dict_class()
for field_name, field in self.fields.items():
if field.read_only and obj is None:
continue
field.initialize(parent=self, field_name=field_name)
key = self.get_field_key(field_name)
value = field.field_to_native(obj, field_name)
method = getattr(self, 'transform_%s' % field_name, None)
if callable(method):
value = method(obj, value)
if field_name not in self.opts.postonly_fields:
ret[key] = value
ret.fields[key] = self.augment_field(field, field_name, key, value)
return ret

A Category model which creates proxy models for related model admin

So I'm having a bit of trouble with trying to create a model that will define dynamic proxy models that manage a related model in the admin site. I know that sentence was confusing, so I'll just share my code instead.
models.py
class Cateogry(models.Model):
name = models.CharField(...)
class Tag(models.Model):
name = models.CharField(...)
category = models.ForeignKey(Cateogry)
What I want to achieve is that in the admin site, instead of having one ModelAdmin for the Tag model, for each category I will have a modeladmin for all related tags. I have achieved this using this answer. Say I have a category named A:
def create_modeladmin(modeladmin, model, name = None):
class Meta:
proxy = True
app_label = model._meta.app_label
attrs = {'__module__': '', 'Meta': Meta}
newmodel = type(name, (model,), attrs)
admin.site.register(newmodel, modeladmin)
return modeladmin
class CatA(TagAdmin):
def queryset(self, request):
qs = super(CatA, self).queryset(request)
return qs.filter(cateogry = Cateogry.objects.filter(name='A'))
create_modeladmin(CatA, name='CategoryAtags', model=Tag)
But this is not good enough, because obviously I still need to manually subclass the TagAdmin model and then run create_modeladmin. What I need to do, is loop over all Category objects, for each one create a dynamic subclass for Tagadmin (named after the category), then create a dynamic proxy model from that, and this is where my head starts spinning.
for cat in Category.objects.all():
NewSubClass = #somehow create subclass of TagAdmin, the name should be '<cat.name>Admin' instead of NewSubClass
create_modeladmin(NewSubClass, name=cat.name, model=Tag)
Any guidance or help would be much appreciated
Dynamic ModelAdmins don't work well together with the way admin registeres models.
I suggest to create subviews in the CategoryAdmin.
from django.conf.urls import patterns, url
from django.contrib import admin
from django.contrib.admin.options import csrf_protect_m
from django.contrib.admin.util import unquote
from django.core.urlresolvers import reverse
from demo_project.demo.models import Category, Tag
class TagAdmin(admin.ModelAdmin):
# as long as the CategoryTagAdmin class has no custom change_list template
# there needs to be a default admin for Tags
pass
admin.site.register(Tag, TagAdmin)
class CategoryTagAdmin(admin.ModelAdmin):
""" A ModelAdmin invoked by a CategoryAdmin"""
read_only_fields = ('category',)
def __init__(self, model, admin_site, category_admin, category_id):
self.model = model
self.admin_site = admin_site
self.category_admin = category_admin
self.category_id = category_id
super(CategoryTagAdmin, self).__init__(model, admin_site)
def queryset(self, request):
return super(CategoryTagAdmin, self).queryset(request).filter(category=self.category_id)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'tag_changelist_link')
def tag_changelist_link(self, obj):
info = self.model._meta.app_label, self.model._meta.module_name
return '<a href="%s" >Tags</a>' % reverse('admin:%s_%s_taglist' % info, args=(obj.id,))
tag_changelist_link.allow_tags = True
tag_changelist_link.short_description = 'Tags'
#csrf_protect_m
def tag_changelist(self, request, *args, **kwargs):
obj_id = unquote(args[0])
info = self.model._meta.app_label, self.model._meta.module_name
category = self.get_object(request, obj_id)
tag_admin = CategoryTagAdmin(Tag, self.admin_site, self, category_id=obj_id )
extra_context = {
'parent': {
'has_change_permission': self.has_change_permission(request, obj_id),
'opts': self.model._meta,
'object': category,
},
}
return tag_admin.changelist_view(request, extra_context)
def get_urls(self):
info = self.model._meta.app_label, self.model._meta.module_name
urls= patterns('',
url(r'^(.+)/tags/$', self.admin_site.admin_view(self.tag_changelist), name='%s_%s_taglist' % info )
)
return urls + super(CategoryAdmin, self).get_urls()
admin.site.register(Category, CategoryAdmin)
The items in the categories changelist have an extra column with a link made by the tag_changelist_link pointing to the CategoryAdmin.tag_changelist. This method creates a CategoryTagAdmin instance with some extras and returns its changelist_view.
This way you have a filtered tag changelist on every category. To fix the breadcrumbs of the tag_changelist view you need to set the CategoryTagAdmin.change_list_template to a own template that {% extends 'admin/change_list.html' %} and overwrites the {% block breadcrumbs %}. That is where you will need the parent variable from the extra_context to create the correct urls.
If you plan to implement a tag_changeview and tag_addview method you need to make sure that the links rendered in variouse admin templates point to the right url (e.g. calling the change_view with a form_url as paramter).
A save_model method on the CategoryTagAdmin can set the default category when adding new tags.
def save_model(self, request, obj, form, change):
obj.category_id = self.category_id
super(CategoryTagAdmin, self).__init__(request, obj, form, change)
If you still want to stick to the apache restart aproach ... Yes you can restart Django. It depends on how you are deploying the instance.
On an apache you can touch the wsgi file that will reload the instance os.utime(path/to/wsgi.py.
When using uwsgi you can use uwsgi.reload().
You can check the source code of Rosetta how they are restarting the instance after the save translations (views.py).
So I found a half-solution.
def create_subclass(baseclass, name):
class Meta:
app_label = 'fun'
attrs = {'__module__': '', 'Meta': Meta, 'cat': name }
newsub = type(name, (baseclass,), attrs)
return newsub
class TagAdmin(admin.ModelAdmin):
list_display = ('name', 'category')
def get_queryset(self, request):
return Tag.objects.filter(category = Category.objects.filter(name=self.cat))
for cat in Category.objects.all():
newsub = create_subclass(TagAdmin, str(cat.name))
create_modeladmin(newsub, model=Tag, name=str(cat.name))
It's working. But every time you add a new category, you need to refresh the server before it shows up (because admin.py is evaluated at runtime). Does anyone know a decent solution to this?

Customize Django Admin Change Form Foreignkey to Include View Record

When selecting a foreignkey in the django admin change form I am trying to add an href that can view the record next to the plus that adds the record.
What I've tried just to get the href to render is I've copied out the admins def render into my own custom widgets file and added it to and subclassed it:
widgets.py
class RelatedFieldWidgetWrapperLink(RelatedFieldWidgetWrapper):
def render(self, name, value, *args, **kwargs):
rel_to = self.rel.to
info = (rel_to._meta.app_label, rel_to._meta.object_name.lower())
try:
related_url = reverse('admin:%s_%s_add' % info, current_app=self.admin_site.name)
except NoReverseMatch:
info = (self.admin_site.root_path, rel_to._meta.app_label, rel_to._meta.object_name.lower())
related_url = '%s%s/%s/add/' % info
self.widget.choices = self.choices
output = [self.widget.render(name, value, *args, **kwargs)]
if self.can_add_related:
# TODO: "id_" is hard-coded here. This should instead use the correct
# API to determine the ID dynamically.
output.append(u'<a href="%s" class="add-another" id="add_id_%s" onclick="return showAddAnotherPopup(this);"> ' % \
(related_url, name))
output.append(u'<img src="%simg/admin/icon_addlink.gif" width="10" height="10" alt="%s"/></a>' % (settings.ADMIN_MEDIA_PREFIX, _('Add Another')))
output.append(u'<a href="%s" class="testing" id="add_id_%s" onclick="#"> ' % \
(related_url, name))
return mark_safe(u''.join(output))
and in admin.py
formfield_overrides = {models.ForeignKey:{'widget':RelatedFieldWidgetWrapperLink}}
however I get thefollowing error:
TypeError
init() takes at least 4 arguments (1 given)
Has anyone run into this problem before?
The RelatedFieldWidgetWrapper widget, and your subclass, are not meant to be used as the widget in formfield_overrides. The __init__ methods have different function signatures, hence the TypeError.
If you look at the code in django.contrib.admin.options, you can see that the RelatedFieldWidgetWrapper widget is instantiated in the model admin's formfield_for_dbfield method, so that it can be passed the arguments rel, admin_site and can_add_related.
I think you may have to override your model admin class' formfield_for_dbfield method, and use your custom RelatedFieldWidgetWrapperLink widget there.
class YourModelAdmin(admin.ModelAdmin):
def formfield_for_dbfield(self, db_field, **kwargs):
<snip>
# ForeignKey or ManyToManyFields
if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
# Combine the field kwargs with any options for formfield_overrides.
# Make sure the passed in **kwargs override anything in
# formfield_overrides because **kwargs is more specific, and should
# always win.
if db_field.__class__ in self.formfield_overrides:
kwargs = dict(self.formfield_overrides[db_field.__class__], **kwargs)
# Get the correct formfield.
if isinstance(db_field, models.ForeignKey):
formfield = self.formfield_for_foreignkey(db_field, request, **kwargs)
elif isinstance(db_field, models.ManyToManyField):
formfield = self.formfield_for_manytomany(db_field, request, **kwargs)
# For non-raw_id fields, wrap the widget with a wrapper that adds
# extra HTML -- the "add other" interface -- to the end of the
# rendered output. formfield can be None if it came from a
# OneToOneField with parent_link=True or a M2M intermediary.
if formfield and db_field.name not in self.raw_id_fields:
related_modeladmin = self.admin_site._registry.get(
db_field.rel.to)
can_add_related = bool(related_modeladmin and
related_modeladmin.has_add_permission(request))
# use your custom widget
formfield.widget = RelatedFieldWidgetWrapperLink(
formfield.widget, db_field.rel, self.admin_site,
can_add_related=can_add_related)
return formfield
<snip>
Other approaches
You may find it cleaner to override the formfield_for_foreignkey method than formfield_for_dbfield.
You may be able to subclass the Select widget, and add your link in it's render method. Your custom select widget would then be wrapped by the RelatedFieldWidgetWrapper. However, I am not sure whether you can produce the view_url inside the scope of the render method.
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from django.forms.widgets import Select
def get_admin_change_url(obj):
ct = ContentType.objects.get_for_model(obj)
change_url_name = 'admin:%s_%s_change' % (ct.app_label, ct.model)
return reverse(change_url_name, args=(obj.id,))
class LinkedSelect(Select):
def render(self, name, value, attrs=None, *args, **kwargs):
output = super(LinkedSelect, self).render(name, value, attrs=attrs, *args, **kwargs)
model = self.choices.field.queryset.model
try:
id = int(value)
obj = model.objects.get(id=id)
view_url = get_admin_change_url(obj)
output += mark_safe(' view ' % (view_url,))
except model.DoesNotExist:
pass
return output
class YourModelAdmin(admin.ModelAdmin):
formfield_overrides = {models.ForeignKey:{'widget':LinkedSelect}}
I improved #Alasdair solution a bit:
from django.contrib.admin.templatetags import admin_static
from django.core import urlresolvers
from django.utils import safestring
from django.utils.translation import ugettext_lazy as _
class LinkedSelect(widgets.Select):
def render(self, name, value, attrs=None, *args, **kwargs):
output = [super(LinkedSelect, self).render(name, value, attrs=attrs, *args, **kwargs)]
model = self.choices.field.queryset.model
try:
obj = model.objects.get(id=value)
change_url = urlresolvers.reverse('admin:%s_%s_change' % (obj._meta.app_label, obj._meta.object_name.lower()), args=(obj.pk,))
output.append(u'<a href="%s" class="change-object" id="change_id_%s"> ' % (change_url, name))
output.append(u'<img src="%s" width="10" height="10" alt="%s"/></a>' % (admin_static.static('admin/img/icon_changelink.gif'), _('Change Object')))
except (model.DoesNotExist, urlresolvers.NoReverseMatch):
pass
return safestring.mark_safe(u''.join(output))
class YourModelAdmin(admin.ModelAdmin):
formfield_overrides = {models.ForeignKey: {'widget': LinkedSelect}}
It uses the same code structure and style as RelatedFieldWidgetWrapper. Additionally, it uses "change" icon instead of just string. It gracefully survives when foreign key points nowhere or where foreign key points to a model which does not have admin interface defined.

Django Model Formset Only first form required

In a modelformset with 3 copies of the form, how do i specify that only the first set is required but the rest can be blank or null?
I've used something like this for inline formsets:
class BaseSomethingFormset(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(BaseSomethingFormset, self).__init__(*args, **kwargs)
self.forms[0].empty_permitted = False
self.forms[0].required = True
The form fields must be by default set to required=False
Matthew Flanagan has a package of things for Django, and in that package is the RequireOneFormset class. You can easily extend this class to require 3 forms instead of one.
Hope that helps you out.
You can subclass BaseModelFormSet so it modifies the first form and makes it required:
from django.forms.models import BaseModelFormSet
class OneRequiredFormSet(BaseModelFormSet):
def _construct_form(self, i, **kwargs):
f = super(OneRequiredFormSet, self)._construct_form(i, **kwargs)
if i == 0:
f.empty_permitted = False
f.required = True
return f
Then you can use the formset keyword argument to tell modelformset_factory to use your new class:
from django.forms.models import modelformset_factory
ParticipantFormSet = modelformset_factory(Participant, extra=1,
form=ParticipantForm,
formset=OneRequiredFormSet)

get_readonly_fields in a TabularInline class in Django?

I'm trying to use get_readonly_fields in a TabularInline class in Django:
class ItemInline(admin.TabularInline):
model = Item
extra = 5
def get_readonly_fields(self, request, obj=None):
if obj:
return ['name']
return self.readonly_fields
This code was taken from another StackOverflow question:
Django admin site: prevent fields from being edited?
However, when it's put in a TabularInline class, the new object forms don't render properly. The goal is to make certain fields read only while still allowing data to be entered in new objects. Any ideas for a workaround or different strategy?
Careful - "obj" is not the inline object, it's the parent. That's arguably a bug - see for example this Django ticket
As a workaround to this issue I have associated a form and a Widget to my Inline:
admin.py:
...
class MasterCouponFileInline(admin.TabularInline):
model = MasterCouponFile
form = MasterCouponFileForm
extra = 0
in Django 2.0:
forms.py
from django import forms
from . import models
from feedback.widgets import DisablePopulatedText
class FeedbackCommentForm(forms.ModelForm):
class Meta:
model = models.MasterCouponFile
fields = ('Comment', ....)
widgets = {
'Comment': DisablePopulatedText,
}
in widgets.py
from django import forms
class DisablePopulatedText(forms.TextInput):
def render(self, name, value, attrs=None, renderer=None):
"""Render the widget as an HTML string."""
if value is not None:
# Just return the value, as normal read_only fields do
# Add Hidden Input otherwise the old fields are still required
HiddenInput = forms.HiddenInput()
return format_html("{}\n"+HiddenInput.render(name, value), self.format_value(value))
else:
return super().render(name, value, attrs, renderer)
older Django Versions:
forms.py
....
class MasterCouponFileForm(forms.ModelForm):
class Meta:
model = MasterCouponFile
def __init__(self, *args, **kwargs):
super(MasterCouponFileForm, self).__init__(*args, **kwargs)
self.fields['range'].widget = DisablePopulatedText(self.instance)
self.fields['quantity'].widget = DisablePopulatedText(self.instance)
in widgets.py
...
from django import forms
from django.forms.util import flatatt
from django.utils.encoding import force_text
class DisablePopulatedText(forms.TextInput):
def __init__(self, obj, attrs=None):
self.object = obj
super(DisablePopulatedText, self).__init__(attrs)
def render(self, name, value, attrs=None):
if value is None:
value = ''
final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
if value != '':
# Only add the 'value' attribute if a value is non-empty.
final_attrs['value'] = force_text(self._format_value(value))
if "__prefix__" not in name and not value:
return format_html('<input{0} disabled />', flatatt(final_attrs))
else:
return format_html('<input{0} />', flatatt(final_attrs))
This is still currently not easily doable due to the fact that obj is the parent model instance not the instance displayed by the inline.
What I did in order to solve this, was to make all the fields, in the inline form, read only and provide a Add/Edit link to a ChangeForm for the inlined model.
Like this
class ChangeFormLinkMixin(object):
def change_form_link(self, instance):
url = reverse('admin:%s_%s_change' % (instance._meta.app_label,
instance._meta.module_name), args=(instance.id,))
# Id == None implies and empty inline object
url = url.replace('None', 'add')
command = _('Add') if url.find('add') > -1 else _('Edit')
return format_html(u'%s' % command, url)
And then in the inline I will have something like this
class ItemInline(ChangeFormLinkMixin, admin.StackedInline):
model = Item
extra = 5
readonly_fields = ['field1',...,'fieldN','change_form_link']
Then in the ChangeForm I'll be able to control the changes the way I want to (I have several states, each of them with a set of editable fields associated).
As others have added, this is a design flaw in django as seen in this Django ticket (thanks Danny W). get_readonly_fields returns the parent object, which is not what we want here.
Since we can't make it readonly, here is my solution to validate it can't be set by the form, using a formset and a clean method:
class ItemInline(admin.TabularInline):
model = Item
formset = ItemInlineFormset
class ItemInlineFormset(forms.models.BaseInlineFormSet):
def clean(self):
super(ItemInlineFormset, self).clean()
for form in self.forms:
if form.instance.some_condition:
form.add_error('some_condition', 'Nope')
You are on the right track. Update self.readonly_fields with a tuple of what fields you want to set as readonly.
class ItemInline(admin.TabularInline):
model = Item
extra = 5
def get_readonly_fields(self, request, obj=None):
# add a tuple of readonly fields
self.readonly_fields += ('field_a', 'field_b')
return self.readonly_fields