Package: django-relations-iterator
django-relations-IteratorProvides utilities for iterating over django model instances hierarchyExample:Simple instances tree clone#models.pyfromdjango.confimportsettingsfromdjango.dbimportmodelsclassMeeting(models.Model):title=models.CharField(max_length=200)time=models.DateTimeField(null=True,blank=True)participants=models.ManyToManyField(settings.AUTH_USER_MODEL,through='Participation',blank=True)classParticipation(models.Model):meeting=models.ForeignKey('Meeting',on_delete=models.CASCADE,related_name='participations')user=models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE,related_name='participations')classInvitation(models.Model):STATUS_SENT='sent'STATUS_ACCEPTED='accepted'STATUS_DECLINED='declined'STATUS_CHOICES=((STATUS_SENT,STATUS_SENT),(STATUS_ACCEPTED,STATUS_ACCEPTED),(STATUS_DECLINED,STATUS_DECLINED),)status=models.CharField(max_length=20,choices=STATUS_CHOICES,default=STATUS_SENT)participation=models.ForeignKey('Participation',on_delete=models.CASCADE,related_name='invitations')classComment(models.Model):meeting=models.ForeignKey('Meeting',on_delete=models.CASCADE,related_name='comments')user=models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE)description=models.TextField(max_length=3000)#clone.pyfromrelations_iteratorimportTreeNode,AbstractVisitor,RelationTreeIterator,ConfigurableRelationTreefrom.modelsimportMeeting# because of config, tree will ignore comments, but will consider all participations and invitationsCLONE_STRUCTURE={'participations':{'invitations':{}}}classCloneVisitor(AbstractVisitor):defvisit(self,node:TreeNode):node.instance.pk=Noneifnode.parentisnotNone:parent_joining_column,instance_joining_column=node.relation.get_joining_columns()[0]setattr(node.instance,instance_joining_column,getattr(node.parent.instance,parent_joining_column))node.instance.save()defclone(instance,config):visitor=CloneVisitor()tree=ConfigurableRelationTree(root=instance,structure=config)fornodeinRelationTreeIterator(tree=tree):visitor.visit(node)meeting=Meeting.objects.last()clone(meeting,CLONE_STRUCTURE)Installationpipinstalldjango-relations-iteratorFeaturesInstance treefromrelations_iteratorimportConfigurableRelationTreeCollects all related instances from model hierarchy accordingly to the provided configfrompprintimportpprintfromdjango.contrib.auth.modelsimportUserfromrelations_iteratorimportConfigurableRelationTreefromtests.meetings.modelsimportMeeting,Participation,Invitation,Commenttom=User.objects.create(username='Tom')jerry=User.objects.create(username='Jerry')meeting=Meeting.objects.create(title='dinner')tom_participation=Participation.objects.create(user_id=tom.id,meeting_id=meeting.id)jerry_participation=Participation.objects.create(user_id=jerry.id,meeting_id=meeting.id)Invitation.objects.create(user_id=jerry.id,meeting_id=meeting.id)Comment.objects.create(user_id=jerry.id,meeting_id=meeting.id)config={'participations':{'invitations':{}}}meeting=Meeting.objects.last()tree=ConfigurableRelationTree(root=instance,structure=config)pprint(tree.tree)# Output:{<TreeNodeforMeeting:Meetingobject(1)>:{<ManyToOneRel:meetings.participation>:{<TreeNodeforParticipation:Participationobject(1)>:{<ManyToOneRel:meetings.invitation>:{<TreeNodeforInvitation:Invitationobject(1)>:{}}},<TreeNodeforParticipation:Participationobject(2)>:{<ManyToOneRel:meetings.invitation>:{}}}}}For provided config tree will build himself only withparticipationsandinvitationsrelations and will ignore any other relations.Tree iteratorfromrelations_iteratorimportRelationTreeIteratorIterates over provided tree and yieds nodes one by oneFor example above it will look likepprint(list(nodefornodeinRelationTreeIterator(tree)))# Output[<TreeNodeforMeeting:Meetingobject(1)>,<TreeNodeforParticipation:Participationobject(1)>,<TreeNodeforInvitation:Invitationobject(1)>,<TreeNodeforParticipation:Participationobject(2)>]Abstract Visitor iteratorfromrelations_iteratorimportAbstractVisitorProvides abstract class, with interface to implement visitor pattern. You must implement.visit(node)method, to complete implementationExamples:Clone visitorfromrelations_iteratorimportTreeNodeclassCloneVisitor(AbstractVisitor):defvisit(self,node:TreeNode):node.instance.pk=Noneifnode.parentisnotNone:parent_joining_column,instance_joining_column=node.relation.get_joining_columns()[0]setattr(node.instance,instance_joining_column,getattr(node.parent.instance,parent_joining_column))node.instance.save()Clone visitor will clone every instance in hierarchy and set proper parent, so it can be used to implement instance hierarchy cloneCLONE_STRUCTURE={'participations':{'invitations':{}}}defclone(instance,config):visitor=CloneVisitor()tree=ConfigurableRelationTree(root=instance,structure=config)fornodeinRelationTreeIterator(tree=tree):visitor.visit(node)clone(meeting,CLONE_STRUCTURE)cloned_meeting=Meeting.objects.last()tree=ConfigurableRelationTree(root=cloned_meeting,structure=CLONE_STRUCTURE)pprint(tree.tree)# Output{<TreeNodeforMeeting:Meetingobject(2)>:{<ManyToOneRel:meetings.participation>:{<TreeNodeforParticipation:Participationobject(3)>:{<ManyToOneRel:meetings.invitation>:{<TreeNodeforInvitation:Invitationobject(2)>:{}}},<TreeNodeforParticipation:Participationobject(3)>:{<ManyToOneRel:meetings.invitation>:{}}}}}Path print visitorPath print visitor will print all parent nodes from root to curent nodeclassPathPrintVisitor(AbstractVisitor):defvisit(self,node:TreeNode):print(list(reversed(self.get_path(node))))defget_path(self,node:TreeNode):path=[node]ifnode.parent:path.extend(self.get_path(node.parent))returnpathvisitor=PathPrintVisitor()fornodeinRelationTreeIterator(tree):visitor.visit(node)# Output[<TreeNodeforMeeting:Meetingobject(2)>][<TreeNodeforMeeting:Meetingobject(2)>,<TreeNodeforParticipation:Participationobject(3)>][<TreeNodeforMeeting:Meetingobject(2)>,<TreeNodeforParticipation:Participationobject(3)>,<TreeNodeforInvitation:Invitationobject(2)>][<TreeNodeforMeeting:Meetingobject(2)>,<TreeNodeforParticipation:Participationobject(4)>]