Skip to content

Instantly share code, notes, and snippets.

@andybak
Forked from luto/reverseadmin.py
Last active August 29, 2015 14:04
Show Gist options
  • Save andybak/80e860673485f5ffdb08 to your computer and use it in GitHub Desktop.
Save andybak/80e860673485f5ffdb08 to your computer and use it in GitHub Desktop.

Revisions

  1. andybak revised this gist Aug 2, 2014. 1 changed file with 9 additions and 133 deletions.
    142 changes: 9 additions & 133 deletions reverseadmin.py
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,9 @@
    """
    adminreverse from here http://djangosnippets.org/snippets/2032/
    changed for working with ForeignKeys
    Fixed for Django 1.6 by @andybak
    Partially Fixed for Django 1.6 by @andybak
    Fully Fixed for Django 1.6 by @luto
    Fieldset support added by @andybak
    reverseadmin
    ============
    @@ -32,22 +34,18 @@ class Person(models.Model):
    home_addr = models.OneToOneField(Address, related_name = 'home_addr')
    other_addr = models.OneToOneField(Address, related_name = 'other_addr')
    This is how standard django admin renders it:
    http://img9.imageshack.us/i/beforetz.png/
    Here is how it looks when using the reverseadmin module:
    http://img408.imageshack.us/i/afterw.png/
    You use reverseadmin in the following way:
    from django.contrib import admin
    from django.db import models
    from models import Person
    from reverseadmin import ReverseModelAdmin
    class AddressForm(models.Form):
    pass
    class PersonAdmin(ReverseModelAdmin):
    inline_type = 'tabular'
    inline_reverse = ('business_addr', ('home_addr', AddressForm), ('other_addr' (
    @@ -64,21 +62,6 @@ class PersonAdmin(ReverseModelAdmin):
    versions.
    """

    # TODO
    # Never got this working but might come back to it
    # class PurchaseOrderAdmin(ReverseModelAdmin):
    #
    # inline_type = 'stacked'
    # inline_reverse = (
    # ('arrival', {
    # 'fields': (
    # ('start_location', 'time_at_start'),
    # ('end_location', 'time_at_end'),
    # ('transit_company', 'transit_person'),
    # ),
    # },),
    # )


    from django.contrib.admin import helpers, ModelAdmin
    from django.contrib.admin.options import InlineModelAdmin, csrf_protect_m, IS_POPUP_VAR
    @@ -192,10 +175,11 @@ def has_add_permission(self, request):

    def get_formset(self, request, obj=None, **kwargs):

    if self.declared_fieldsets:
    fields = flatten_fieldsets(self.declared_fieldsets)
    if 'fields' in kwargs:
    fields = kwargs.pop('fields')
    else:
    fields = None
    fields = flatten_fieldsets(self.get_fieldsets(request, obj))

    if self.exclude is None:
    exclude = []
    else:
    @@ -370,111 +354,3 @@ def add_view(self, request, form_url='', extra_context=None):
    }
    context.update(extra_context or {})
    return self.render_change_form(request, context, form_url=form_url, add=True)

    # TODO Not sure we need this
    #
    # @csrf_protect_m
    # @transaction.atomic
    # def change_view(self, request, object_id, form_url='', extra_context=None):
    # "The 'change' admin view for this model."
    # model = self.model
    # opts = model._meta
    #
    # obj = self.get_object(request, unquote(object_id))
    #
    # if not self.has_change_permission(request, obj):
    # raise PermissionDenied
    #
    # if obj is None:
    # raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_text(opts.verbose_name), 'key': escape(object_id)})
    #
    # if request.method == 'POST' and "_saveasnew" in request.POST:
    # return self.add_view(request, form_url=reverse('admin:%s_%s_add' %
    # (opts.app_label, opts.model_name),
    # current_app=self.admin_site.name))
    #
    # ModelForm = self.get_form(request, obj)
    # formsets = []
    # inline_instances = self.get_inline_instances(request, obj)
    # if request.method == 'POST':
    # form = ModelForm(request.POST, request.FILES, instance=obj)
    # if form.is_valid():
    # form_validated = True
    # new_object = self.save_form(request, form, change=True)
    # else:
    # form_validated = False
    # new_object = obj
    # prefixes = {}
    # for FormSet, inline in zip(self.get_formsets(request, new_object), inline_instances):
    # prefix = FormSet.get_default_prefix()
    # prefixes[prefix] = prefixes.get(prefix, 0) + 1
    # if prefixes[prefix] != 1 or not prefix:
    # prefix = "%s-%s" % (prefix, prefixes[prefix])
    # formset = FormSet(request.POST, request.FILES,
    # instance=new_object, prefix=prefix,
    # queryset=inline.get_queryset(request))
    #
    # formsets.append(formset)
    #
    # if all_valid(formsets) and form_validated:
    #
    # # Here is the modified code.
    #
    # for formset, inline in zip(formsets, self.get_inline_instances(request)):
    # if not isinstance(inline, ReverseInlineModelAdmin):
    # continue
    # objects = formset.save()
    # if len(objects)==1:
    # setattr(new_object, inline.parent_fk_name, objects[0])
    # # End modified code
    #
    #
    # self.save_model(request, new_object, form, True)
    # self.save_related(request, form, formsets, True)
    # change_message = self.construct_change_message(request, form, formsets)
    # self.log_change(request, new_object, change_message)
    # return self.response_change(request, new_object)
    #
    # else:
    # form = ModelForm(instance=obj)
    # prefixes = {}
    # for FormSet, inline in zip(self.get_formsets(request, obj), inline_instances):
    # prefix = FormSet.get_default_prefix()
    # prefixes[prefix] = prefixes.get(prefix, 0) + 1
    # if prefixes[prefix] != 1 or not prefix:
    # prefix = "%s-%s" % (prefix, prefixes[prefix])
    # formset = FormSet(instance=obj, prefix=prefix,
    # queryset=inline.get_queryset(request))
    # formsets.append(formset)
    #
    # adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj),
    # self.get_prepopulated_fields(request, obj),
    # self.get_readonly_fields(request, obj),
    # model_admin=self)
    # media = self.media + adminForm.media
    #
    # inline_admin_formsets = []
    # for inline, formset in zip(inline_instances, formsets):
    # fieldsets = list(inline.get_fieldsets(request, obj))
    # readonly = list(inline.get_readonly_fields(request, obj))
    # prepopulated = dict(inline.get_prepopulated_fields(request, obj))
    # inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
    # fieldsets, prepopulated, readonly, model_admin=self)
    # inline_admin_formsets.append(inline_admin_formset)
    # media = media + inline_admin_formset.media
    #
    # context = {
    # 'title': _('Change %s') % force_text(opts.verbose_name),
    # 'adminform': adminForm,
    # 'object_id': object_id,
    # 'original': obj,
    # 'is_popup': IS_POPUP_VAR in request.REQUEST,
    # 'media': media,
    # 'inline_admin_formsets': inline_admin_formsets,
    # 'errors': helpers.AdminErrorList(form, formsets),
    # 'app_label': opts.app_label,
    # 'preserved_filters': self.get_preserved_filters(request),
    # }
    # context.update(extra_context or {})
    # return self.render_change_form(request, context, change=True, obj=obj, form_url=form_url)
    #
  2. @luto luto revised this gist Jul 4, 2014. 1 changed file with 3 additions and 2 deletions.
    5 changes: 3 additions & 2 deletions reverseadmin.py
    Original file line number Diff line number Diff line change
    @@ -239,12 +239,13 @@ def __init__(self, model, admin_site):
    for field_config in self.inline_reverse:
    kwargs = {}
    if isinstance(field_config, tuple):
    field_name = field_config[0]
    if isinstance(field_config[1], dict):
    kwargs = field_config[1]
    elif isinstance(field_config[1], ModelForm):
    kwargs['form'] = field_config[1]

    field_name = field_config[0]
    else:
    field_name = field_config
    field = model._meta.get_field(field_name)

    if isinstance(field, (OneToOneField, ForeignKey)):
  3. andybak created this gist Jul 4, 2014.
    479 changes: 479 additions & 0 deletions reverseadmin.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,479 @@
    """
    adminreverse from here http://djangosnippets.org/snippets/2032/
    changed for working with ForeignKeys
    Fixed for Django 1.6 by @andybak
    reverseadmin
    ============
    Module that makes django admin handle OneToOneFields in a better way.
    A common use case for one-to-one relationships is to "embed" a model
    inside another one. For example, a Person may have multiple foreign
    keys pointing to an Address entity, one home address, one business
    address and so on. Django admin displays those relations using select
    boxes, letting the user choose which address entity to connect to a
    person. A more natural way to handle the relationship is using
    inlines. However, since the foreign key is placed on the owning
    entity, django admins standard inline classes can't be used. Which is
    why I created this module that implements "reverse inlines" for this
    use case.
    Example:
    from django.db import models
    class Address(models.Model):
    street = models.CharField(max_length = 255)
    zipcode = models.CharField(max_length = 10)
    city = models.CharField(max_length = 255)
    class Person(models.Model):
    name = models.CharField(max_length = 255)
    business_addr = models.ForeignKey(Address,
    related_name = 'business_addr')
    home_addr = models.OneToOneField(Address, related_name = 'home_addr')
    other_addr = models.OneToOneField(Address, related_name = 'other_addr')
    This is how standard django admin renders it:
    http://img9.imageshack.us/i/beforetz.png/
    Here is how it looks when using the reverseadmin module:
    http://img408.imageshack.us/i/afterw.png/
    You use reverseadmin in the following way:
    from django.contrib import admin
    from django.db import models
    from models import Person
    from reverseadmin import ReverseModelAdmin
    class AddressForm(models.Form):
    pass
    class PersonAdmin(ReverseModelAdmin):
    inline_type = 'tabular'
    inline_reverse = ('business_addr', ('home_addr', AddressForm), ('other_addr' (
    'form': OtherForm
    'exclude': ()
    )))
    admin.site.register(Person, PersonAdmin)
    inline_type can be either "tabular" or "stacked" for tabular and
    stacked inlines respectively.
    The module is designed to work with Django 1.6. Since it hooks into
    the internals of the admin package, it may not work with later Django
    versions.
    """

    # TODO
    # Never got this working but might come back to it
    # class PurchaseOrderAdmin(ReverseModelAdmin):
    #
    # inline_type = 'stacked'
    # inline_reverse = (
    # ('arrival', {
    # 'fields': (
    # ('start_location', 'time_at_start'),
    # ('end_location', 'time_at_end'),
    # ('transit_company', 'transit_person'),
    # ),
    # },),
    # )


    from django.contrib.admin import helpers, ModelAdmin
    from django.contrib.admin.options import InlineModelAdmin, csrf_protect_m, IS_POPUP_VAR
    from django.contrib.admin.util import flatten_fieldsets, unquote
    from django.core.urlresolvers import reverse
    from django.db import transaction, models
    from django.db.models import OneToOneField, ForeignKey
    from django.forms import ModelForm
    from django.forms.formsets import all_valid
    from django.forms.models import BaseModelFormSet, modelformset_factory
    from django.http import Http404
    from django.utils.encoding import force_unicode, force_text
    from django.utils.functional import curry
    from django.utils.html import escape
    from django.utils.safestring import mark_safe
    from django.utils.translation import ugettext as _
    from django.core.exceptions import PermissionDenied

    class ReverseInlineFormSet(BaseModelFormSet):
    """
    A formset with either a single object or a single empty
    form. Since the formset is used to render a required OneToOne
    relation, the forms must not be empty.
    """
    model = None
    parent_fk_name = ''

    def __init__(
    self,
    data=None,
    files=None,
    instance=None,
    prefix=None,
    queryset=None,
    save_as_new=False):

    object = getattr(instance, self.parent_fk_name)

    if object:
    qs = self.model.objects.filter(pk = object.id)
    self.extra = 0
    self.max_num = 0
    else:
    qs = self.model.objects.filter(pk = -1)
    self.extra = 0
    self.max_num = 0

    super(ReverseInlineFormSet, self).__init__(
    data,
    files,
    prefix=prefix,
    queryset=qs
    )

    for form in self.forms:
    form.empty_permitted = False

    def reverse_inlineformset_factory(
    model,
    parent_fk_name,
    form=ModelForm,
    fields=None,
    exclude=None,
    formfield_callback=lambda f: f.formfield()):

    kwargs = {
    'form': form,
    'formfield_callback': formfield_callback,
    'formset': ReverseInlineFormSet,
    'extra': 0,
    'can_delete': False,
    'can_order': False,
    'fields': fields,
    'exclude': exclude,
    'max_num': 0,
    }

    FormSet = modelformset_factory(model, **kwargs)
    FormSet.parent_fk_name = parent_fk_name
    return FormSet

    class ReverseInlineModelAdmin(InlineModelAdmin):
    """
    Use the name and the help_text of the owning models field to
    render the verbose_name and verbose_name_plural texts.
    """
    def __init__(
    self,
    parent_model,
    parent_fk_name,
    model,
    admin_site,
    inline_type):

    self.template = 'admin/edit_inline/%s.html' % inline_type
    self.parent_fk_name = parent_fk_name
    self.model = model
    field_descriptor = getattr(parent_model, self.parent_fk_name)
    field = field_descriptor.field

    self.verbose_name_plural = field.verbose_name.title()
    self.verbose_name = field.help_text

    if not self.verbose_name:
    self.verbose_name = self.verbose_name_plural

    super(ReverseInlineModelAdmin, self).__init__(parent_model, admin_site)

    def has_add_permission(self, request):
    return False

    def get_formset(self, request, obj=None, **kwargs):

    if self.declared_fieldsets:
    fields = flatten_fieldsets(self.declared_fieldsets)
    else:
    fields = None
    if self.exclude is None:
    exclude = []
    else:
    exclude = list(self.exclude)

    # if exclude is an empty list we use None, since that's the actual
    # default
    exclude = (exclude + kwargs.get("exclude", [])) or None

    defaults = {
    "form": self.form,
    "fields": fields,
    "exclude": exclude,
    "formfield_callback": curry(self.formfield_for_dbfield, request=request),
    }

    defaults.update(kwargs)
    return reverse_inlineformset_factory(
    self.model,
    self.parent_fk_name,
    **defaults
    )

    class ReverseModelAdmin(ModelAdmin):
    """
    Patched ModelAdmin class. The add_view method is overridden to
    allow the reverse inline formsets to be saved before the parent
    model.
    """
    def __init__(self, model, admin_site):

    super(ReverseModelAdmin, self).__init__(model, admin_site)

    # Initialize reverse_inline_instances

    if self.exclude is None:
    self.exclude = []

    self.reverse_inline_instances = []

    for field_config in self.inline_reverse:
    kwargs = {}
    if isinstance(field_config, tuple):
    if isinstance(field_config[1], dict):
    kwargs = field_config[1]
    elif isinstance(field_config[1], ModelForm):
    kwargs['form'] = field_config[1]

    field_name = field_config[0]
    field = model._meta.get_field(field_name)

    if isinstance(field, (OneToOneField, ForeignKey)):
    name = field.name
    parent = field.related.parent_model
    inline = ReverseInlineModelAdmin(
    model,
    name,
    parent,
    admin_site,
    self.inline_type
    )

    if kwargs:
    inline.__dict__.update(kwargs)

    self.reverse_inline_instances.append(inline)
    self.exclude.append(name)

    def get_inline_instances(self, request, obj=None):
    inline_instances = super(ReverseModelAdmin, self).get_inline_instances(request, obj)
    return self.reverse_inline_instances + inline_instances

    @csrf_protect_m
    @transaction.atomic
    def add_view(self, request, form_url='', extra_context=None):
    "The 'add' admin view for this model."
    model = self.model
    opts = model._meta

    if not self.has_add_permission(request):
    raise PermissionDenied

    ModelForm = self.get_form(request)
    formsets = []
    inline_instances = self.get_inline_instances(request, None)
    if request.method == 'POST':
    form = ModelForm(request.POST, request.FILES)
    if form.is_valid():
    new_object = self.save_form(request, form, change=False)
    form_validated = True
    else:
    form_validated = False
    new_object = self.model()
    prefixes = {}
    for FormSet, inline in zip(self.get_formsets(request), inline_instances):
    prefix = FormSet.get_default_prefix()
    prefixes[prefix] = prefixes.get(prefix, 0) + 1
    if prefixes[prefix] != 1 or not prefix:
    prefix = "%s-%s" % (prefix, prefixes[prefix])
    formset = FormSet(data=request.POST, files=request.FILES,
    instance=new_object,
    save_as_new="_saveasnew" in request.POST,
    prefix=prefix, queryset=inline.get_queryset(request))
    formsets.append(formset)

    if all_valid(formsets) and form_validated:

    # Here is the modified code.

    for formset, inline in zip(formsets, self.get_inline_instances(request)):
    if not isinstance(inline, ReverseInlineModelAdmin):
    continue
    objects = formset.save()
    if len(objects)==1:
    setattr(new_object, inline.parent_fk_name, objects[0])

    # End modified code

    self.save_model(request, new_object, form, False)
    self.save_related(request, form, formsets, False)

    self.log_addition(request, new_object)
    return self.response_add(request, new_object)
    else:
    # Prepare the dict of initial data from the request.
    # We have to special-case M2Ms as a list of comma-separated PKs.
    initial = dict(request.GET.items())
    for k in initial:
    try:
    f = opts.get_field(k)
    except models.FieldDoesNotExist:
    continue
    if isinstance(f, models.ManyToManyField):
    initial[k] = initial[k].split(",")
    form = ModelForm(initial=initial)
    prefixes = {}
    for FormSet, inline in zip(self.get_formsets(request), inline_instances):
    prefix = FormSet.get_default_prefix()
    prefixes[prefix] = prefixes.get(prefix, 0) + 1
    if prefixes[prefix] != 1 or not prefix:
    prefix = "%s-%s" % (prefix, prefixes[prefix])
    formset = FormSet(instance=self.model(), prefix=prefix,
    queryset=inline.get_queryset(request))
    formsets.append(formset)

    adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)),
    self.get_prepopulated_fields(request),
    self.get_readonly_fields(request),
    model_admin=self)
    media = self.media + adminForm.media

    inline_admin_formsets = []
    for inline, formset in zip(inline_instances, formsets):
    fieldsets = list(inline.get_fieldsets(request))
    readonly = list(inline.get_readonly_fields(request))
    prepopulated = dict(inline.get_prepopulated_fields(request))
    inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
    fieldsets, prepopulated, readonly, model_admin=self)
    inline_admin_formsets.append(inline_admin_formset)
    media = media + inline_admin_formset.media

    context = {
    'title': _('Add %s') % force_text(opts.verbose_name),
    'adminform': adminForm,
    'is_popup': IS_POPUP_VAR in request.REQUEST,
    'media': media,
    'inline_admin_formsets': inline_admin_formsets,
    'errors': helpers.AdminErrorList(form, formsets),
    'app_label': opts.app_label,
    'preserved_filters': self.get_preserved_filters(request),
    }
    context.update(extra_context or {})
    return self.render_change_form(request, context, form_url=form_url, add=True)

    # TODO Not sure we need this
    #
    # @csrf_protect_m
    # @transaction.atomic
    # def change_view(self, request, object_id, form_url='', extra_context=None):
    # "The 'change' admin view for this model."
    # model = self.model
    # opts = model._meta
    #
    # obj = self.get_object(request, unquote(object_id))
    #
    # if not self.has_change_permission(request, obj):
    # raise PermissionDenied
    #
    # if obj is None:
    # raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_text(opts.verbose_name), 'key': escape(object_id)})
    #
    # if request.method == 'POST' and "_saveasnew" in request.POST:
    # return self.add_view(request, form_url=reverse('admin:%s_%s_add' %
    # (opts.app_label, opts.model_name),
    # current_app=self.admin_site.name))
    #
    # ModelForm = self.get_form(request, obj)
    # formsets = []
    # inline_instances = self.get_inline_instances(request, obj)
    # if request.method == 'POST':
    # form = ModelForm(request.POST, request.FILES, instance=obj)
    # if form.is_valid():
    # form_validated = True
    # new_object = self.save_form(request, form, change=True)
    # else:
    # form_validated = False
    # new_object = obj
    # prefixes = {}
    # for FormSet, inline in zip(self.get_formsets(request, new_object), inline_instances):
    # prefix = FormSet.get_default_prefix()
    # prefixes[prefix] = prefixes.get(prefix, 0) + 1
    # if prefixes[prefix] != 1 or not prefix:
    # prefix = "%s-%s" % (prefix, prefixes[prefix])
    # formset = FormSet(request.POST, request.FILES,
    # instance=new_object, prefix=prefix,
    # queryset=inline.get_queryset(request))
    #
    # formsets.append(formset)
    #
    # if all_valid(formsets) and form_validated:
    #
    # # Here is the modified code.
    #
    # for formset, inline in zip(formsets, self.get_inline_instances(request)):
    # if not isinstance(inline, ReverseInlineModelAdmin):
    # continue
    # objects = formset.save()
    # if len(objects)==1:
    # setattr(new_object, inline.parent_fk_name, objects[0])
    # # End modified code
    #
    #
    # self.save_model(request, new_object, form, True)
    # self.save_related(request, form, formsets, True)
    # change_message = self.construct_change_message(request, form, formsets)
    # self.log_change(request, new_object, change_message)
    # return self.response_change(request, new_object)
    #
    # else:
    # form = ModelForm(instance=obj)
    # prefixes = {}
    # for FormSet, inline in zip(self.get_formsets(request, obj), inline_instances):
    # prefix = FormSet.get_default_prefix()
    # prefixes[prefix] = prefixes.get(prefix, 0) + 1
    # if prefixes[prefix] != 1 or not prefix:
    # prefix = "%s-%s" % (prefix, prefixes[prefix])
    # formset = FormSet(instance=obj, prefix=prefix,
    # queryset=inline.get_queryset(request))
    # formsets.append(formset)
    #
    # adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj),
    # self.get_prepopulated_fields(request, obj),
    # self.get_readonly_fields(request, obj),
    # model_admin=self)
    # media = self.media + adminForm.media
    #
    # inline_admin_formsets = []
    # for inline, formset in zip(inline_instances, formsets):
    # fieldsets = list(inline.get_fieldsets(request, obj))
    # readonly = list(inline.get_readonly_fields(request, obj))
    # prepopulated = dict(inline.get_prepopulated_fields(request, obj))
    # inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
    # fieldsets, prepopulated, readonly, model_admin=self)
    # inline_admin_formsets.append(inline_admin_formset)
    # media = media + inline_admin_formset.media
    #
    # context = {
    # 'title': _('Change %s') % force_text(opts.verbose_name),
    # 'adminform': adminForm,
    # 'object_id': object_id,
    # 'original': obj,
    # 'is_popup': IS_POPUP_VAR in request.REQUEST,
    # 'media': media,
    # 'inline_admin_formsets': inline_admin_formsets,
    # 'errors': helpers.AdminErrorList(form, formsets),
    # 'app_label': opts.app_label,
    # 'preserved_filters': self.get_preserved_filters(request),
    # }
    # context.update(extra_context or {})
    # return self.render_change_form(request, context, change=True, obj=obj, form_url=form_url)
    #