我有一个django ap,它有一个相当复杂的模型设置.我最终使用多级合成来创建分层模型.所有的关系是一对一的,所以我可以使用继承,但我选择不这样做,我将从我的模型的对象组成中受益,这意味着我可以做类似的事情
product.outerframe.top.cost
这使得我必须进行复杂的计算,组织得更好.
但是,这种模式安排使得使用django admin很棘手.我基本上有一个直通表,即外框表只是一堆外键到其他表(每个表都有唯一约束).我最终了解了ModelAdmin的add_view()和change_view()方法,这非常困难.
使用django管理员时,是否有更简单的方法来处理多对多/通过表格?
表格的排列方式如下:
产品>外框,内框,玻璃,其他
外框>顶部,底部,侧面等
内框架>顶部,底部,侧面等
玻璃> glass_type等
其他>配件等
这是我的模特:
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
混入:
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)
##################
还有一个拉头管理员!
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)
解决方法:
我没有完全处理你冗长的add_view方法,但你的一般问题的答案只是“不”.管理员没有提供任何处理多层异构层次结构的好方法.通过内联很好地处理两层层次结构,因此您可以轻松地进行处理,以便通过编辑任何一层中的对象,您可以方便地管理下一层中的相关对象;但除此之外没什么.
多年来一直有a ticket开放为管理员添加嵌套内联支持,这将有助于处理这种情况.但是有许多棘手的边缘情况,并且很难使UI易于理解,因此补丁从未达到提交就绪状态.
在某些时候,您的数据模型的复杂性超出了通用管理界面可以处理的良好可用性,并且您最好只编写自己的自定义管理界面.主要是管理员只是建立在ModelForms和InlineModelFormsets之上,所以它并不像你想象的那样难以构建你自己的工作方式;与尝试大量定制管理员相比,它通常更容易(并且具有更好的结果).
我还应该提一下,通过表可以使用admin inlines多对多(即使through表是隐式的,而不是它自己的模型类),因为如何访问隐式创建的直通模型并不是很明显:
class MyM2MInline(admin.TabularInline):
model = SomeModel.m2m_field.through