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)
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?
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.*
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