Tool to iterate over django relation tree
Project description
django-relations-Iterator
Provides utilities for iterating over django model instances hierarchy. Provides easy out-of-the-box way to clone django instances.
Reasoning and solution for use case with cloning - https://hackernoon.com/the-smart-way-to-clone-django-instances
Example:
Simple instances tree clone
#models.py
from django.conf import settings
from django.db import models
class Meeting(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)
class Participation(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')
class Invitation(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')
class Comment(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.py
from relations_iterator import clone, CloneVisitor
from .models import Meeting
# because of config, tree will ignore comments, but will consider all participations and invitations
CLONE_STRUCTURE = {
'participations': {
'invitations': {}
}
}
meeting = Meeting.objects.last()
clone(meeting, CLONE_STRUCTURE, CloneVisitor())
Customizing cloning process
# Example: you want to set title for cloned Meeting as {original_title}-COPY
# and set time of the instance to None
class CustomCloneVisitor(CloneVisitor):
@singledispatchmethod
def customize(self, instance):
pass
@customize.register
def _(self, instance: Meeting):
instance.title = f'{instance.title}-COPY'
instance.time = None
Installation
pip install django-relations-iterator
Features
Instance tree
from relations_iterator import ConfigurableRelationTree
Collects all related instances from model hierarchy accordingly to the provided config
from pprint import pprint
from django.contrib.auth.models import User
from relations_iterator import ConfigurableRelationTree
from tests.meetings.models import Meeting, Participation, Invitation, Comment
tom = 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:
{
<TreeNode for Meeting: Meeting object (1)>: {
<ManyToOneRel: meetings.participation>: {
<TreeNode for Participation: Participation object (1)>: {
<ManyToOneRel: meetings.invitation>: {
<TreeNode for Invitation: Invitation object (1)>: {}
}
},
<TreeNode for Participation: Participation object (2)>: {
<ManyToOneRel: meetings.invitation>: {}
}
}
}
}
For provided config tree will build himself only with participations
and invitations
relations and will ignore any other relations.
Tree iterator
from relations_iterator import RelationTreeIterator
Iterates over provided tree and yieds nodes one by one
For example above it will look like
pprint(list(node for node in RelationTreeIterator(tree)))
# Output
[<TreeNode for Meeting: Meeting object (1)>,
<TreeNode for Participation: Participation object (1)>,
<TreeNode for Invitation: Invitation object (1)>,
<TreeNode for Participation: Participation object (2)>]
Abstract Visitor iterator
from relations_iterator import AbstractVisitor
Provides abstract class, with interface to implement visitor pattern. You must implement .visit(node)
method, to complete implementation
Instances cloning feature
from relations_iterator import clone, CloneVisitor
Provides function to clone instances and simple CloneVisitor class, just as explained below in examples section.
clone
function accepts 3 arguments:
instance
- django Model instance, that needs to be clonedconfig
- config dictionary of the structure that needs to be clonedvisitor
- visitor instance.CloneVisitor
can be used directly or you can customize it and pass your own implementation
Config explanation:
Example:
# Config for cloning Meeting instance, we want to clone also participation's and invitations
config = {
'participations': { # related name for `Participation` model, that have fk to Meeting
'invitations': {} # related name for `Invitation` model, that have fk to `Participation` model
}
}
# All other relations will be skipped, as they are not listed in config
Examples:
Clone visitor full implementation
from django.db.models import Model
from relations_iterator import TreeNode, AbstractVisitor
class CloneVisitor(AbstractVisitor):
def visit(self, node: TreeNode):
node.instance.pk = None
if node.parent is not None:
parent_joining_field, instance_joining_field = node.relation.get_joining_fields()[0]
setattr(
node.instance,
instance_joining_field.attname,
parent_joining_field.value_from_object(node.parent.instance)
)
self.customize(node.instance)
node.instance.save()
def customize(self, instance: Model) -> None:
pass
Clone visitor will clone every instance in hierarchy and set proper parent, so it can be used to implement instance hierarchy clone
CLONE_STRUCTURE = {
'participations': {
'invitations': {}
}
}
def clone(instance, config):
visitor = CloneVisitor()
tree = ConfigurableRelationTree(root=instance, structure=config)
for node in RelationTreeIterator(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
{
<TreeNode for Meeting: Meeting object (2)>: {
<ManyToOneRel: meetings.participation>: {
<TreeNode for Participation: Participation object (3)>: {
<ManyToOneRel: meetings.invitation>: {
<TreeNode for Invitation: Invitation object (2)>: {}
}
},
<TreeNode for Participation: Participation object (3)>: {
<ManyToOneRel: meetings.invitation>: {}
}
}
}
}
Path print visitor
Path print visitor will print all parent nodes from root to curent node
class PathPrintVisitor(AbstractVisitor):
def visit(self, node: TreeNode):
print(list(reversed(self.get_path(node))))
def get_path(self, node: TreeNode):
path = [node]
if node.parent:
path.extend(self.get_path(node.parent))
return path
visitor = PathPrintVisitor()
for node in RelationTreeIterator(tree):
visitor.visit(node)
# Output
[<TreeNode for Meeting: Meeting object (2)>]
[<TreeNode for Meeting: Meeting object (2)>, <TreeNode for Participation: Participation object (3)>]
[<TreeNode for Meeting: Meeting object (2)>, <TreeNode for Participation: Participation object (3)>, <TreeNode for Invitation: Invitation object (2)>]
[<TreeNode for Meeting: Meeting object (2)>, <TreeNode for Participation: Participation object (4)>]
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for django_relations_iterator-0.0.5.tar.gz
Algorithm | Hash digest | |
---|---|---|
SHA256 | 036e7d058f2782a37471b2b8135e6a2813edd1f1b0d59b543b609b449826c5e8 |
|
MD5 | 30fee5ef73115de53033e33029107f93 |
|
BLAKE2b-256 | 0f8dbcfed82be149531167506a2d5f5365c8eec8b06020baed255d4de7f83039 |
Hashes for django_relations_iterator-0.0.5-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 3f04306f77152bf37c0e3218c8bf655ef9a427b8f8f7406d457e76455e87e616 |
|
MD5 | d430777ee94e3aa5885f7ab79a2d6cbf |
|
BLAKE2b-256 | d84e9218ec63f6b95f0021505ecfb05b514785f170c8c2572eded752c30b5a8b |