Question

I have two classes, one called phase1 and one called phase2. phase2 has an instance of phase1 in it (as a Foreign Key relationship). Now I want to make another class, called POObject, that has a Foreign Key value called purchase to either a phase1 or a phase2 object. That is, I want to be able to create a POObject with purchase set to a phase1 instance, and then later change it to a phase2 instance.

I believe this is called Duck Typing, but I'm not sure if there's an easy way to do it in Django. I know Python allows for Duck Typing quite easily, but I'm guessing the Database won't allow it without at least some additional work.

Any advice on how do to this would be much appreciated.

After implementing orlenko's suggestion, I'm having trouble adding new items. Here are the views that deal with creating a new instance. The way it works is by creating a new purchase in new, and then using the instance of that purchase in new2 to create a POObject.

def new(request):
  if request.method == 'POST':
    form = PurchaseOrderForm(request.POST)
    if form.is_valid():
      new_po = form.save()
      return HttpResponseRedirect('/workflow/%s' %new_po.request_number)
    else: 
      return render(request, 'input.html', {'input_type': 'Purchase Order','formset': form, 'error': True})
  else: 
    form = PurchaseOrderForm()
    return render(request, 'input.html', {'input_type': 'Purchase Order','formset': form,})

def new2(request, number):
  po=PurchaseOrder.objects.get(pk=number)
  if request.method == 'POST':
    form = WorkflowForm(request.POST, initial={'purchase': PurchaseOrder.objects.get(pk=number), 'state': 'request'})
    if form.is_valid():
      new_flow = form.save()
      return HttpResponse('Good')
    else:
      print form.errors
      return render(request, 'new-workflow.html', {'form': form, 'purchase': po})
  else:
    form = WorkflowForm(initial={'purchase': PurchaseOrder.objects.get(pk=number), 'state': 'request'})
    return render(request, 'new-workflow.html', {'form': form, 'purchase': po, 'type': 'New'})

The appropriate form class is:

class WorkflowForm(ModelForm):
  purchase1=forms.ModelChoiceField(queryset=PurchaseOrder.objects.all(), required=False)
  purchase2=forms.ModelChoiceField(queryset=Phase2.objects.all(), required=False)
  details = forms.CharField(required=False)

  class Meta:
    model = POObject
Was it helpful?

Solution 2

Model classes are a little more restrictive than the rest of Python code, because they have to translate into actual database tables and relationships between them.

But your idea with "flexible FK" can be implemented like this:

class Phase1(Model):
  #...

class Phase2(Model):
  #...

class POObject(Model):
  phase1 = ForeignKey(Phase1, null=True)
  phase2 = ForeignKey(Phase2, null=True, related_name='+')

  def get_phase(self):
    if self.phase2_id: # more likely case first
       return self.phase2
    return self.phase1

  def set_phase(self, phase):
    # Detect which type it is
    if isinstance(phase, Phase2):
      self.phase2 = phase
      self.phase1 = None
    else:
      self.phase2 = None
      self.phase1 = phase

   phase = property(get_phase, set_phase)

Here, we have two optional FKs and use one or the other. The phase property hides the details, and lets you treat the two FKs as one. When you assign a Phase record to po_object.phase, the code figures out what table to point FK to.

OTHER TIPS

I think Lalo's proposal is right. You can try Django content types:

from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic

class Phase1(models.Model):
    #...

class Phase2(models.Model):
    #...

class POObject(models.Model):
    # Standard names.
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    # Your 'generic' foreign key. 
    purchase = generic.GenericForeignKey('content_type', 'object_id')

Then, you can do this:

phase1 = Phase1.objects.get(id='xyz')
phase2 = Phase2.objects.get(id='abc')
p1 = POObject(purchase=phase1)
p1.save()
p2 = POObject(purchase=phase2)
p2.save()

You have to include django.contrib.contenttypes to your INSTALLED_APPS settings (it's included by default).

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top