Django proxy model and ForeignKey
Asked Answered
B

7

21

How to make entry.category to be instance of CategoryProxy? See code for details:

class Category(models.Model): pass

class Entry(models.Model):
    category = models.ForeignKey(Category)

class EntryProxy(Entry):
    class Meta:
        proxy = True

class CategoryProxy(Category):
    class Meta:
        proxy = True

entry = EntryProxy.objects.get(pk=1)
entry.category # !!! I want CategoryProxy instance here

Cast from Category to CategoryProxy is ok too, but I am not very familiar with ORM internals to properly copy internal state...

EDIT. Reason: I added method to CategoryProxy and want to use him:

EntryProxy.objects.get(pk=1).category.method_at_category_proxy()

EDIT 2. Currently I implemented it like this:

EntryProxy._meta.get_field_by_name('category')[0].rel.to = CategoryProxy

but it looks terrible...

Boutwell answered 8/10, 2010 at 15:10 Comment(2)
Any particular reason why you want to do this?Pilgrimage
I added method to CategoryProxy and want to use him like EntryProxy.objects.get(pk=1).category.method_at_category_proxy()Boutwell
M
15

To switch from a model class to a proxy class without hitting the database:

class EntryProxy(Entry):
    @property
    def category(self):
        new_inst = EntryProxy()
        new_inst.__dict__ = super(EntryProxy, self).category.__dict__
        return new_inst

edit: the snippet above seems not working on django 1.4.

Since django 1.4, I take all value fields manually like this:

class EntryProxy(Entry):
    @property
    def category(self):
        category = super(EntryProxy, self).category
        new_inst = EntryProxy()
        for attr in [f.attname for f in category.__class__._meta.fields] + ['_state']:
            setattr(new_inst, attr, getattr(category, attr))
        return new_inst

To switch from a queryset to a child proxy class without hitting database:

class CategoryProxy(Category):
    @property
    def entry_set(self):
        qs = super(CategoryProxy, self).entry_set
        qs.model = EntryProxy
        return qs
Merrow answered 8/8, 2011 at 20:59 Comment(2)
+1 +1 thank you! How did you come up with this? I was curious if you had a blog post or something explaining in detail how QuerySet.model works...Communicant
For the first I found it here, in stackoverflow, for the second, I found it my self in a bpython shell test. Pleased to see it helped.Merrow
C
9

This is an open Django issue: #10961 (Allow users to override forward and reverse relationships on proxy models with ForeignKey fields)

You can work around it by resetting the fields in question after you define the proxy models:

EntryProxy.add_to_class('category', CategoryProxy)
Corin answered 26/7, 2013 at 1:24 Comment(1)
Open for 13 yearsTableau
M
2

None of the current solutions (including the accepted one) work with Django 2.0.

Building on Matt Schinckel's work on overriding proxy model relations, here's a solution that will work with Django 2.0 and 2.1.

Mariannmarianna answered 30/11, 2018 at 20:13 Comment(0)
P
1

This question already has an accepted answer, but wanted to post this for anyone who may come searching.

You can patch the model at runtime with the new field so that relations work as expected. A full example can be seen here - https://gist.github.com/carymrobbins/8721082

from django.db.models.fields.related import ReverseSingleRelatedObjectDescriptor

def override_model_field(model, field, field_name, column_name):
    """Force override a field in a Django Model.
    Usage: override_model_field(
        MyModel, models.ForeignKey(OtherModel), 'other', 'other_id')
    :type model: django.db.models.base.ModelBase
    :type field: django.db.models.fields.Field
    :type field_name: basestring
    :type column_name: basestring
    """
    field.name = field_name
    field.attname = column_name
    for i, f in enumerate(model._meta.fields):
        if f.name == field_name:
            model._meta.fields[i] = field
            break
    else:
        raise TypeError('Model {!r} does not have a field {!r}.'
                        .format(model, field_name))
    model.add_to_class(field_name,
                       ReverseSingleRelatedObjectDescriptor(field))
Perjured answered 30/1, 2014 at 22:15 Comment(0)
L
0

You can add proxy model to ForeignKey:

class Entry(models.Model):
    category = models.ForeignKey(CategoryProxy, on_delete=models.CASCADE)

and now:

entry = EntryProxy.objects.get(pk=1)
entry.category # CategoryProxy instance
Ludly answered 25/9, 2021 at 18:37 Comment(0)
F
-2

Define a property category on EntryProxy that looks up the CategoryProxy by its id:

class EntryProxy(Entry):
    @property
    def category(self):
        cid = super(EntryProxy, self).category.id
        return CategoryProxy.objects.get(id=cid)

    class Meta:
        proxy = True
Fanti answered 8/10, 2010 at 17:3 Comment(1)
This implies a database hit for every Category lookup! What if we're looping?Selector
G
-2

Adapting Bernd Petersohn's answer slightly, we then have:

class EntryProxy(Entry):
    @property
    def category(self):
        return CategoryProxy.objects.get(id=self.category_id)

This ought to be more economical with the database. For added improvements you could set a private attribute (self._category) the first time the method is called, then return that all subsequent times.

Gunfire answered 27/1, 2012 at 16:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.