Django: cannot get an Model instance from ForwardManyToOneDescriptor (ForeignKey) - python

I have the following code in accounts/signals/__init__.py:
from django.db.models.signals import post_save
from django.dispatch import receiver
from orders.models import Order
from accounts.models import Balance
#receiver(post_save, sender=Order)
def update_referral_balance(sender, **kwargs):
if len(sender.user.referrals_set.all()):
# TODO: Add referralTransaction
new_referral_revenue = sender.user.referrals_set.get().revenue
revenue_from_trade = \
new_referral_revenue - sender.old_referral_revenue
balance, created = \
Balance.objects.get(user=sender.user, currency=sender.currency)
balance.balance += revenue_from_trade
balance.save()
Now, when running tests I am getting the following
error:======================================================================
ERROR: test_orders_with_approved_payments (payments.tests.test_views.PaymentReleaseTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/pipeline/source/payments/tests/test_views.py", line 75, in setUp
self.order.save()
File "/pipeline/source/orders/models.py", line 63, in save
super(Order, self).save(*args, **kwargs)
File "/usr/local/lib/python3.5/site-packages/safedelete/models.py", line 64, in save
super(Model, self).save(**kwargs)
File "/usr/local/lib/python3.5/site-packages/django/db/models/base.py", line 708, in save
force_update=force_update, update_fields=update_fields)
File "/usr/local/lib/python3.5/site-packages/django/db/models/base.py", line 745, in save_base
update_fields=update_fields, raw=raw, using=using)
File "/usr/local/lib/python3.5/site-packages/django/dispatch/dispatcher.py", line 192, in send
response = receiver(signal=self, sender=sender, **named)
File "/pipeline/source/accounts/signals/__init__.py", line 9, in update_referral_balance
if len(sender.user.referral_set.all()):
AttributeError: 'ForwardManyToOneDescriptor' object has no attribute 'referral_set'
And indeed, when running through it in debugger, I see that the sender.user attribute is something of instance ForwardManyToOneDescriptor:
ipdb> pprint(sender.__dict__['user'].__dict__)
{'cache_name': '_user_cache',
'field': <django.db.models.fields.related.ForeignKey: user>}
What am I doing wrong?
EDIT: My Order Model:
class Order(TimeStampedModel, SoftDeletableModel, UniqueFieldMixin):
USD = "USD"
RUB = "RUB"
EUR = "EUR"
BUY = 1
SELL = 0
TYPES = (
(SELL, 'SELL'),
(BUY, 'BUY'),
)
# Todo: inherit from BTC base?, move lengths to settings?
order_type = models.IntegerField(choices=TYPES, default=BUY)
amount_cash = models.DecimalField(max_digits=12, decimal_places=2)
amount_btc = models.DecimalField(max_digits=18, decimal_places=8)
currency = models.ForeignKey(Currency)
payment_window = models.IntegerField(default=settings.PAYMENT_WINDOW)
user = models.ForeignKey(User, related_name='orders')
is_paid = models.BooleanField(default=False)
is_released = models.BooleanField(default=False)
is_completed = models.BooleanField(default=False)
is_failed = models.BooleanField(default=False)
unique_reference = models.CharField(
max_length=settings.UNIQUE_REFERENCE_LENGTH, unique=True)
admin_comment = models.CharField(max_length=200)
payment_preference = models.ForeignKey('payments.PaymentPreference',
default=None,
null=True)
class Meta:
ordering = ['-created_on']
def save(self, *args, **kwargs):
self.unique_reference = \
self.gen_unique_value(
lambda x: get_random_string(x),
lambda x: Order.objects.filter(unique_reference=x).count(),
settings.UNIQUE_REFERENCE_LENGTH
)
self.convert_coin_to_cash()
if 'is_completed' in kwargs and\
kwargs['is_completed'] and\
not self.is_completed:
self.old_referral_revenue = \
self.user.referral_set.get().revenue
super(Order, self).save(*args, **kwargs)
def convert_coin_to_cash(self):
self.amount_btc = Decimal(self.amount_btc)
queryset = Price.objects.filter().order_by('-id')[:2]
price_sell = [price for price in queryset if price.type == Price.SELL]
price_buy = [price for price in queryset if price.type == Price.BUY]
# Below calculation affect real money the client pays
assert all([len(price_sell),
price_sell[0].price_usd,
price_buy[0].price_rub,
price_buy[0].price_eur])
assert all([len(price_buy),
price_buy[0].price_usd,
price_buy[0].price_rub,
price_buy[0].price_eur])
# TODO: Make this logic more generic,
# TODO: migrate to using currency through payment_preference
# SELL
self.amount_cash = Decimal(self.amount_btc)
if self.order_type == Order.SELL and self.currency.code == Order.USD:
self.amount_cash *= price_buy[0].price_usd
elif self.order_type == Order.SELL and self.currency.code == Order.RUB:
self.amount_cash *= price_buy[0].price_rub
elif self.order_type == Order.SELL and self.currency.code == Order.EUR:
self.amount_cash *= price_buy[0].price_eur
# BUY
if self.order_type == Order.BUY and self.currency.code == Order.USD:
self.amount_cash *= price_sell[0].price_usd
elif self.order_type == Order.BUY and self.currency.code == Order.RUB:
self.amount_cash *= price_sell[0].price_rub
elif self.order_type == Order.BUY and self.currency.code == Order.EUR:
self.amount_cash *= price_sell[0].price_eur
self.amount_cash = money_format(self.amount_cash)
#property
def is_buy(self):
return self.order_type
#property
def payment_deadline(self):
"""returns datetime of payment_deadline (creation + payment_window)"""
# TODO: Use this for pay until message on 'order success' screen
return self.created_on + timedelta(minutes=self.payment_window)
#property
def expired(self):
"""Is expired if payment_deadline is exceeded and it's not paid yet"""
# TODO: validate this business rule
# TODO: Refactor, it is unreasonable to have different standards of
# time in the DB
return (timezone.now() > self.payment_deadline) and\
(not self.is_paid) and not self.is_released
#property
def payment_status_frozen(self):
"""return a boolean indicating if order can be updated
Order is frozen if it is expired or has been paid
"""
# TODO: validate this business rule
return self.expired or \
(self.is_paid and
self.payment_set.last() and
self.payment_set.last().
payment_preference.
payment_method.is_internal)
#property
def withdrawal_address_frozen(self):
"""return bool whether the withdraw address can
be changed"""
return self.is_released
#property
def has_withdraw_address(self):
"""return a boolean indicating if order has a withdraw adrress defined
"""
# TODO: Validate this business rule
return len(self.address_set.all()) > 0
#property
def withdraw_address(self):
addr = None
if self.has_withdraw_address:
addr = self.transaction_set.first().address_to.address
return addr
def __str__(self):
return "{} {} {} BTC {} {}".format(self.user.username or
self.user.profile.phone,
self.order_type,
self.amount_btc,
self.amount_cash,
self.currency)

The sender argument is the model class the signal has connected to. As you can see from the signals docs, in post_save the instance is passed in a separate argument unsurprisingly called instance.
You should write your handler like this:
#receiver(post_save, sender=Order)
def update_referral_balance(sender, instance, **kwargs):
if len(instance.user.referrals_set.all()):
etc, changing sender to instance throughout.

Related

How to filter Django object to get top X number of objects with highest property value

So I have a class called Hero with 150 objects. Each object has a property Winrate. I want to get the top 12 heros based on winrate.
class Hero(models.Model):
hero_name = models.CharField(max_length=20, default = 'Dota 2 Hero')
hero_id = models.IntegerField()
def __str__(self):
return str(self.hero_id)
def get_winrate(self):
wins = len(Match.objects.filter(heros_won = Hero.objects.get(hero_id = self.hero_id)))
losses = len(Match.objects.filter(heros_lost = Hero.objects.get(hero_id = self.hero_id)))
if wins + losses != 0:
return round((wins / (wins + losses)),2)
else:
return 0
winrate = property(get_winrate)
I tried alot of filters but couldn't get it to work.
I would make winrate an attribute of your Hero class as following.
class Hero(models.Model):
hero_name = models.CharField(max_length=20, default = 'Dota 2 Hero')
hero_id = models.IntegerField()
winrate = models.IntegerField()
def _get_winrate(self):
wins = len(Match.objects.filter(heros_won = Hero.objects.get(hero_id = self.hero_id)))
losses = len(Match.objects.filter(heros_lost = Hero.objects.get(hero_id = self.hero_id)))
if wins + losses != 0:
return round((wins / (wins + losses)),2)
else:
return 0
def save(*args, **kwargs):
self.winrate = self._getwinrate()
return super().save(*args, **kwargs)
Then you'll be able to order your request.
super_heroes = Hero.objects.order_by('-winrate')[:12]
EDIT: you shouldn't use len() on a queryset but count() like this:
wins = Match.objects.filter(heros_won=self.pk).count()
Why don't you use the natural primary key instead of this hero_id?

Unit testing method that raises and exception

I try to unit-test if method responsible for returning price of given product raises an exception if we pass bad item_id - find_price_of_given_id.
Test:
import unittest
from Automat import Automat
from Bank import Bank
from Item import Item
from exceptions.NoItemException import NoItemException
class AutomatTest(unittest.TestCase):
def test_checkPriceOfGivenID(self):
bank = Bank()
automat = Automat(bank)
Cola = Item(2)
automat.add_object(Cola)
self.assertEqual(automat.find_price_of_given_id(30), 2)
def test_checkPriceOfGivenIDWithInvalidID(self):
bank = Bank()
automat = Automat(bank)
Cola = Item(2)
automat.add_object(Cola)
self.assertRaises(NoItemException, automat.find_price_of_given_id(31))
Automat class:
if __name__ == '__main__':
unittest.main()
from Item import Item
from exceptions.NoItemException import NoItemException
from exceptions.NoProperAmountException import NoProperAmountException
from Coin import Coin
from decimal import *
class Automat:
def __init__(self, _bank, objects=None):
self.item_id = 30
self.bank = _bank
if objects is None:
objects = {}
self.objects = objects
self.purchaseItemBank = []
def add_object(self, obj: Item):
id_to_assign = self.item_id
self.objects.update({id_to_assign: obj})
self.item_id += 1
return id_to_assign
def find_price_of_given_id(self, item_id):
if self.objects.get(item_id) is not None:
return self.objects.get(item_id).get_price()
else:
raise NoItemException
def find_amount_of_given_id(self, item_id):
if self.objects.get(item_id) is not None:
return self.objects.get(item_id).get_amount()
else:
raise NoItemException
def checkIfAmountIsPositive(self, item_id):
if self.objects.get(item_id) is not None:
var = True if self.objects.get(item_id).get_amount() > 0 else False
return var
else:
raise NoItemException
def withdrawItem(self, item_id):
self.objects.get(item_id).decrease()
def getAmountOfGivenNominal(self, coinValue):
counter = 0
for iterator in self.bank.bank:
if iterator.getCoinValue() == coinValue:
counter += 1
return counter
Unfortunately I got
Error
Traceback (most recent call last):
File "C:\ProgramData\Anaconda3\lib\unittest\case.py", line 59, in testPartExecutor
yield
File "C:\ProgramData\Anaconda3\lib\unittest\case.py", line 615, in run
testMethod()
File "C:\Users\Admin\PycharmProjects\vending-machine\AutomatTest.py", line 22, in test_checkPriceOfGivenIDWithInvalidID
self.assertRaises(NoItemException, automat.find_price_of_given_id(31))
File "C:\Users\Admin\PycharmProjects\vending-machine\Automat.py", line 28, in find_price_of_given_id
raise NoItemException
exceptions.NoItemException.NoItemException
Ran 1 test in 0.003s
FAILED (errors=1)
Process finished with exit code 1
Item class:
class Item:
def __init__(self, price, amount=5):
self.amount = amount
self.price = price
def get_price(self):
return self.price
def get_amount(self):
return self.amount
def decrease(self):
self.amount -= 1
def __str__(self):
return f"{self.amount} # {self.price}"
self.assertRaises(NoItemException, automat.find_price_of_given_id(31))
You're calling the method, and then pass its return value into assertRaises; but before that can happen, the exception is already raised. This is not how you use assertRaises. You can use it in two ways:
Pass the method you want to call and its arguments to assertRaises:
self.assertRaises(NoItemException, automat.find_price_of_given_id, 31)
Note: no (), you're not calling it yourself!
Use it as context manager:
with self.assertRaises(NoItemException):
automat.find_price_of_given_id(31)

Pass QuerySet object id to model to get self

I am trying to trigger a method in my models to run by hitting a url.When it hits it returns and error saying "unsupported operand type(s) for -: 'datetime.datetime' and 'NoneType'".
views.py
from django.shortcuts import render
from rest_framework import viewsets
from .serializers import EntrySerializer
from .models import Entry
class EntryView(viewsets.ModelViewSet):
serializer_class = EntrySerializer
queryset = Entry.objects.all()
entryId = Entry.objects.filter(end_time__isnull=True).values('id')
# returns <QuerySet [{'id': 8}]>
for id in entryId:
entryIdNum = id['id']
#returns 8
entry = Entry()
def ToggleView(request):
entry.toggle_paused()
entry.save()
models.py
from django.db import models
from django.db.models import F, ExpressionWrapper, fields
from django.utils import timezone
from dateutil.relativedelta import relativedelta
from decimal import Decimal
class Entry(models.Model):
start_time = models.DateTimeField()
end_time = models.DateTimeField(blank=True, null=True, db_index=True)
seconds_paused = models.PositiveIntegerField(default=0)
pause_time = models.DateTimeField(blank=True, null=True)
comments = models.TextField(blank=True)
date_updated = models.DateTimeField(auto_now=True)
hours = models.DecimalField(max_digits=11, decimal_places=5, default=0)
def _str_(self):
return self.TUID
#property
def total_hours(self):
"""
Determined the total number of hours worked in this entry
"""
total = self.get_total_seconds() / 3600.0
# in case seconds paused are greater than the elapsed time
if total < 0:
total = 0
return total
#property
def is_paused(self):
"""
Determine whether or not this entry is paused
"""
return bool(self.pause_time)
def pause(self):
"""
If this entry is not paused, pause it.
"""
print('Pause Hit')
if not self.is_paused:
self.pause_time = timezone.now()
def pause_all(self):
"""
Pause all open entries
"""
entries = self.user.timepiece_entries.filter(end_time__isnull=True)
for entry in entries:
entry.pause()
entry.save()
def unpause(self, date=None):
print('Unpause Hit')
if self.is_paused:
if not date:
date = timezone.now()
delta = date - self.pause_time
self.seconds_paused += delta.seconds
self.pause_time = None
def toggle_paused(self):
"""
Toggle the paused state of this entry. If the entry is already paused,
it will be unpaused; if it is not paused, it will be paused.
"""
print('Toggle Pause Hit')
if self.is_paused:
self.unpause()
else:
self.pause()
def check_overlap(self, entry_b, **kwargs):
"""Return True if the two entries overlap."""
consider_pause = kwargs.get('pause', True)
entry_a = self
# if entries are open, consider them to be closed right now
if not entry_a.end_time or not entry_b.end_time:
return False
# Check the two entries against each other
start_inside = entry_a.start_time > entry_b.start_time \
and entry_a.start_time < entry_b.end_time
end_inside = entry_a.end_time > entry_b.start_time \
and entry_a.end_time < entry_b.end_time
a_is_inside = entry_a.start_time > entry_b.start_time \
and entry_a.end_time < entry_b.end_time
b_is_inside = entry_a.start_time < entry_b.start_time \
and entry_a.end_time > entry_b.end_time
overlap = start_inside or end_inside or a_is_inside or b_is_inside
if not consider_pause:
return overlap
else:
if overlap:
max_end = max(entry_a.end_time, entry_b.end_time)
min_start = min(entry_a.start_time, entry_b.start_time)
diff = max_end - min_start
diff = diff.seconds + diff.days * 86400
total = entry_a.get_total_seconds() + entry_b.get_total_seconds() - 1
if total >= diff:
return True
return False
def is_overlapping(self):
if self.start_time and self.end_time:
entries = self.user.timepiece_entries.filter(
Q(end_time__range=(self.start_time, self.end_time)) |
Q(start_time__range=(self.start_time, self.end_time)) |
Q(start_time__lte=self.start_time, end_time__gte=self.end_time)
)
totals = entries.aggregate(max=Max('end_time'), min=Min('start_time'))
totals['total'] = 0
for entry in entries:
totals['total'] = totals['total'] + entry.get_total_seconds()
totals['diff'] = totals['max'] - totals['min']
totals['diff'] = totals['diff'].seconds + \
totals['diff'].days * 86400
if totals['total'] > totals['diff']:
return True
else:
return False
else:
return None
def save(self, *args, **kwargs):
self.hours = Decimal('%.5f' % round(self.total_hours, 5))
super(Entry, self).save(*args, **kwargs)
def get_total_seconds(self):
"""
Determines the total number of seconds between the starting and
ending times of this entry. If the entry is paused, the end_time is
assumed to be the pause time. If the entry is active but not paused,
the end_time is assumed to be now.
"""
start = self.start_time
end = self.end_time
if not end:
if self.is_paused:
end = self.pause_time
print(end)
print('------------------')
else:
end = timezone.now()
delta = end - start
if self.is_paused:
# get_paused_seconds() takes elapsed time into account, which we do not want
# in this case, so subtract seconds_paused instead to account for previous pauses
seconds = delta.seconds - self.seconds_paused
else:
seconds = delta.seconds - self.get_paused_seconds()
return seconds + (delta.days * 86400)
I know the error is comming from the method get_total_seconds(). When self.start_time is called it returns Entry object (None). How can I pass entryIdNum or entryId from views to the model so it knows what self is?

What is error all about? and why?

class account(object):
__duser_id = ''
__duser_name =''
__duser_no = ''
def __init__(self, default, entry_name, password, user_id='', user_name='', user_no=''):
if type(default) != bool:
raise Exception("Error 0x1: type(default) is boolean ")
if default == False:
self.__user_id = user_id
self.__user_name = user_name
self.__user_no = user_no
else:
self.__user_id = __duser_id
self.__user_name = __duser_name
self.__user_no = __duser_no
self.__entry_name = entry_name
self.__password = password
def dset(self, duser_id=__duser_id, duser_name=__duser_name, duser_no=__duser_no):
__duser_id = duser_id
__duser_name = duser_name
__duser_no = duser_no
return (__duser_id, __duser_name, __duser_no)
def dget(self):
return (__duser_id, __duser_name, __duser_no)
def set(self, user_name=self.__user_name, user_id=self.__user_id, user_no=self.__user_no, password=self.__password):
self.__user_id = user_id
self.__user_name = user_name
self.__user_no = user_no
self.__password = password
return (self.__user_id, self.__user_name, self.__user_no, self.password)
def get(self):
return (self.__user_id, self.__user_name, self.__user_no, self.password)
if __name__ == '__main__':
gmail = account(default=True, entry_name='gmail', password='pass***')
print(gmail.dget())
print(gmail.get())
out put is:
Traceback (most recent call last):
File "interface.py", line 1, in
class account(object):
File "interface.py", line 30, in account
def set(self, user_name=self.__user_name, user_id=self.__user_id, user_no=self.__user_no, password=self.__password):
NameError: name 'self' is not defined
Ok o got it.
but there is another one i just changed code.
This is a decorator with arbitrary number of arguments and keyword
arguments
def user_no_is_number(func):
def wrapper(*args, **kargs):
if 'user_no' in kargs:
if type(kargs['user_no']) != int:
raise Exception('Error 1x0: user_no must contains only numbers.')
else:
return func(*args, **kargs)
return wrapper
#staticmethod
#user_no_is_number
def dset(user_id=None, user_name=None, user_no=None):
if user_id:
account.__duser_id = user_id
if user_name:
account.__duser_name = user_name
if user_no:
account.__duser_no = user_no
return (account.__duser_id, account.__duser_name, account.__duser_no)
but the dset() function return always None
*I think there is problem with arbitrary keywords parameters. by using **kargs in decorator parameter it becomes dictionary and by again passing **kargs it just return values of that dictionary.*

Django Many to Many and admin

I have a django ap which has a rather complicated model setup. I ended up using multi level composition to create a hierarchical model. All the relations are one to one, so I could have use inheritance but I chose not to so that i would benefit from having object composition for my models, this means I can do things like
product.outerframe.top.cost
which make the complicated calculations I have to preform, a lot better organised.
However, This model arrangement makes using the django admin tricky. I basically have a through table, i.e the outerframe table is just a bunch of foreign keys to other tables (with unique constraint on each). I ended up oerriding the add_view() and change_view() methods of ModelAdmin, which is pretty hard.
Is there an easier way to deal with many to many / through tables when using the django admin?
The tables are arranged like so:
Product > outerframe, innerframe, glass, other
outerframe > top, bottom, side etc.
innerframe > top, bottom, side etc.
glass > glass_type etc.
other > accessories etc.
Here are my models:
class Product(mixins.ProductVariables):
name = models.CharField(max_length=255)
sku = models.CharField(max_length=100, unique=True, db_index=True)
image = thumbnail.ImageField(upload_to='product_images', blank=True)
description = models.TextField(blank=True)
group = models.ForeignKey('ProductGroup', related_name='products', null=True)
hidden = models.BooleanField(default=False)
product_specific_mark_up = models.DecimalField(default=1.0, max_digits=5,decimal_places=2)
# Methods for totals
def total_material_cost(self, width, height, options):
return sum([
self.outerframe.cost(width, height, options),
self.innerframe.cost(width, height, options),
self.glass.cost(width, height, options),
self.other.cost(width, height, options),
])
def total_labour_time(self, width, height, options):
return sum([
self.outerframe.labour_time(width, height, options),
self.innerframe.labour_time(width, height, options),
self.glass.labour_time(width, height, options),
self.other.labour_time(width, height, options),
])
def total_co2_output(self, width, height, options):
return sum([
self.outerframe.co2_output(width, height, options),
self.innerframe.co2_output(width, height, options),
self.glass.co2_output(width, height, options),
self.other.co2_output(width, height, options),
])
#property
def max_overall_width(self):
return 1000
#property
def max_overall_height(self):
return 1000
def __unicode__(self):
return self.name
class OuterFrame(models.Model, mixins.GetFieldsMixin, mixins.GetRelatedClassesMixin):
top = models.OneToOneField(mixins.TopFrame)
bottom = models.OneToOneField(mixins.BottomFrame)
side = models.OneToOneField(mixins.SideFrame)
accessories = models.OneToOneField(mixins.Accessories)
flashing = models.OneToOneField(mixins.Flashing)
silicone = models.OneToOneField(mixins.Silicone)
product = models.OneToOneField(Product)
def cost(self, width, height, options):
#accessories_cost = (self.accessories.cost if options['accessories'] else 0)
#flashing_cost = (self.flashing.cost if options['flashing'] else 0)
#silicone_cost = (self.silicone.cost if options['silicone'] else 0)
return sum([
self.top.cost * (width / 1000),
self.bottom.cost * (width / 1000),
self.side.cost * (width*2 / 1000),
#accessories_cost,
#flashing_cost,
#silicone_cost,
])
def labour_time(self, width, height, options):
return datetime.timedelta(minutes=100)
def CO2_output(self, width, height, options):
return 100 # some kg measurement
#classmethod
def get_fields(cls):
options = cls._meta
fields = {}
for field in options.fields:
if field.name == 'product':
continue
if isinstance(field, models.OneToOneField):
related_cls = field.rel.to
related_fields = fields_for_model(related_cls, fields=related_cls.get_fields())
fields.update( { related_cls.__name__ + '_' + name:field for name, field in related_fields.iteritems() })
return fields
class InnerFrame(models.Model, mixins.GetFieldsMixin, mixins.GetRelatedClassesMixin):
top = models.OneToOneField(mixins.TopFrame)
bottom = models.OneToOneField(mixins.BottomFrame)
side = models.OneToOneField(mixins.SideFrame)
accessories = models.OneToOneField(mixins.Accessories)
product = models.OneToOneField(Product)
def cost(self, width, height, options):
#accessories_cost = (self.accessories.cost if options['accessories'] else 0)
print self.top.cost
return sum([
self.top.cost * (width / 1000),
self.bottom.cost * (width / 1000),
self.side.cost * (width*2 / 1000),
# accessories_cost,
])
def labour_time(self, width, height, options):
return datetime.timedelta(minutes=100)
def CO2_output(self, width, height, options):
return 100 # some kg measurement
class Glass(models.Model, mixins.GetRelatedClassesMixin):
glass_type_a = models.OneToOneField(mixins.GlassTypeA)
glass_type_b = models.OneToOneField(mixins.GlassTypeB)
enhanced = models.OneToOneField(mixins.Enhanced)
laminate = models.OneToOneField(mixins.Laminate)
low_iron = models.OneToOneField(mixins.LowIron)
privacy = models.OneToOneField(mixins.Privacy)
anti_slip = models.OneToOneField(mixins.AntiSlip)
heat_film_mirror = models.OneToOneField(mixins.HeatMirrorField)
posished_edges = models.OneToOneField(mixins.PolishedEdges)
product = models.OneToOneField(Product)
def cost(self, width, height, options):
return sum([
])
def labour_time(self, width, height, options):
return datetime.timedelta(minutes=100)
def CO2_output(self, width, height, options):
return 100 # some kg measurement
class Other(models.Model, mixins.GetRelatedClassesMixin):
num_packages = models.OneToOneField(mixins.NumberPackages)
product = models.OneToOneField(Product)
def cost(self, width, height, options):
return 100
def labour_time(self, width, height, options):
return datetime.timedelta(minutes=100)
def CO2_output(self, width, height, options):
return 100 # some kg measurement
mixins:
class TimeCostMixin(models.Model, GetFieldsMixin):
cost = models.DecimalField(default=0.0, max_digits=10, decimal_places=2)
time = models.TimeField(default=datetime.timedelta(0))
class Meta:
abstract = True
##### Frame #####
class FrameComponentMixin(TimeCostMixin):
external_width = models.IntegerField(default=0)
material_weight = models.DecimalField(default=0.0, max_digits=10,decimal_places=2)
u_value = models.DecimalField(default=0.0, max_digits=10,decimal_places=2)
class Meta:
abstract = True
class TopFrame(FrameComponentMixin):
pass
class BottomFrame(FrameComponentMixin):
pass
class SideFrame(FrameComponentMixin):
pass
class Accessories(TimeCostMixin):
material_weight = models.DecimalField(default=0.0,max_digits=10,decimal_places=2)
class Flashing(TimeCostMixin):
pass
class Silicone(TimeCostMixin):
labour_time = models.DecimalField(default=0.0, max_digits=10,decimal_places=2)
#################
##### Glass #####
class GlassTypeA(TimeCostMixin):
material_weight = models.DecimalField(default=0.0, max_digits=10,decimal_places=2)
u_value = models.DecimalField(default=0.0, max_digits=10,decimal_places=2)
class GlassTypeB(TimeCostMixin):
material_weight = models.DecimalField(default=0.0, max_digits=10,decimal_places=2)
u_value = models.DecimalField(default=0.0, max_digits=10,decimal_places=2)
class Enhanced(TimeCostMixin):
material_weight = models.DecimalField(default=0.0, max_digits=10,decimal_places=2)
class Laminate(TimeCostMixin):
material_weight = models.DecimalField(default=0.0, max_digits=10,decimal_places=2)
class LowIron(TimeCostMixin):
pass
class Privacy(TimeCostMixin):
pass
class AntiSlip(TimeCostMixin):
pass
class HeatMirrorField(TimeCostMixin):
u_value = models.DecimalField(default=0.0, max_digits=10,decimal_places=2)
class PolishedEdges(models.Model):
cost = models.DecimalField(default=0.0, max_digits=10, decimal_places=2)
##################
##### other #####
class NumberPackages(models.Model):
number_of_packages = models.IntegerField(default=0)
##################
and a hair pulling admin!
class ProductAdmin(AdminImageMixin, admin.ModelAdmin):
inlines = [ProductDownloadInline, ProductConfigurationInline]
add_form_template = 'admin/products/add_form.html'
change_form_template = 'admin/products/add_form.html'
#csrf_protect_m
#transaction.atomic
def add_view(self, request, form_url='', extra_context=None):
extra_context = extra_context or {}
"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)
#####
outer_frame_forms = [
modelform_factory(cls)(request.POST, prefix='OuterFrame_'+cls.__name__)
for cls in models.OuterFrame.get_related_classes(exclude=['product'])
]
inner_frame_forms = [
modelform_factory(cls)(request.POST, prefix='InnerFrame'+cls.__name__)
for cls in models.InnerFrame.get_related_classes(exclude=['product'])
]
glass_forms = [
modelform_factory(cls)(request.POST, prefix='InnerFrame'+cls.__name__)
for cls in models.Glass.get_related_classes(exclude=['product'])
]
other_forms = [
modelform_factory(cls)(request.POST, prefix='InnerFrame'+cls.__name__)
for cls in models.Other.get_related_classes(exclude=['product'])
]
#####
if all_valid(formsets
+outer_frame_forms
+inner_frame_forms
+glass_forms
+other_forms
) and form_validated:
self.save_model(request, new_object, form, False)
self.save_related(request, form, formsets, False)
self.log_addition(request, new_object)
##### save object hierichy #####
# inner frame
inner_frame = models.InnerFrame()
inner_frame.product = new_object
mapping = {f.rel.to:f.name for f in models.InnerFrame._meta.fields if f.name not in ['id','product']}
for f in inner_frame_forms:
obj = f.save()
setattr(inner_frame, mapping[obj.__class__], obj)
inner_frame.save()
# outer frame
outer_frame = models.OuterFrame()
outer_frame.product = new_object
mapping = {f.rel.to:f.name for f in models.OuterFrame._meta.fields if f.name not in ['id','product']}
for f in outer_frame_forms:
obj = f.save()
setattr(outer_frame, mapping[obj.__class__], obj)
outer_frame.save()
# glass
glass = models.Glass()
glass.product = new_object
mapping = {f.rel.to:f.name for f in models.Glass._meta.fields if f.name not in ['id','product']}
for f in glass_forms:
obj = f.save()
setattr(glass, mapping[obj.__class__], obj)
glass.save()
# other
other = models.Other()
other.product = new_object
mapping = {f.rel.to:f.name for f in models.Other._meta.fields if f.name not in ['id','product']}
for f in other_forms:
obj = f.save()
setattr(other, mapping[obj.__class__], obj)
other.save()
#################################
return self.response_add(request, new_object)
else:
forms = SortedDict({})
forms['Outer Frame Variables'] = {
cls.__name__: modelform_factory(cls)(prefix='OuterFrame_'+cls.__name__)
for cls in models.OuterFrame.get_related_classes(exclude=['product'])
}
forms['Inner Frame Variables'] = {
cls.__name__: modelform_factory(cls)(prefix='InnerFrame'+cls.__name__)
for cls in models.InnerFrame.get_related_classes(exclude=['product'])
}
forms['Glass Variables'] = {
cls.__name__: modelform_factory(cls)(prefix='InnerFrame'+cls.__name__)
for cls in models.Glass.get_related_classes(exclude=['product'])
}
forms['Other Variables'] = {
cls.__name__: modelform_factory(cls)(prefix='InnerFrame'+cls.__name__)
for cls in models.Other.get_related_classes(exclude=['product'])
}
extra_context['forms'] = forms
# 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)
I haven't fully processed your lengthy add_view method, but the answer to your general question is simply "No." The admin doesn't provide any good way to handle multi-layer heterogeneous hierarchies. Two-layer hierarchies are handled nicely by inlines, and so you can easily make it so that from editing an object in any one layer, you can conveniently manage related objects in the next layer down; but nothing beyond that.
There has been a ticket open for years to add nested-inline support to the admin, which would help to handle this situation. But there are lots of tricky edge-cases and it's very hard to make the UI understandable, so the patch has never reached a commit-ready state.
At some point the complexity of your data model is just beyond what the generic admin interface can handle with good usability, and you're better off just writing your own customized admin interface. Mostly the admin is just built on top of ModelForms and InlineModelFormsets, so it's not as hard as you might think to just build your own that works the way you want; it's often easier (and with better results) than trying to heavily customize the admin.
I should also mention that it is possible to use admin inlines for many-to-many through tables (even if the through table is implicit, not its own model class), as it's not immediately obvious how to access the implicitly-created through model:
class MyM2MInline(admin.TabularInline):
model = SomeModel.m2m_field.through

Categories