Skip to main content

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 cloned
  • config - config dictionary of the structure that needs to be cloned
  • visitor - 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

django_relations_iterator-0.0.5.tar.gz (5.7 kB view details)

Uploaded Source

Built Distribution

File details

Details for the file django_relations_iterator-0.0.5.tar.gz.

File metadata

File hashes

Hashes for django_relations_iterator-0.0.5.tar.gz
Algorithm Hash digest
SHA256 036e7d058f2782a37471b2b8135e6a2813edd1f1b0d59b543b609b449826c5e8
MD5 30fee5ef73115de53033e33029107f93
BLAKE2b-256 0f8dbcfed82be149531167506a2d5f5365c8eec8b06020baed255d4de7f83039

See more details on using hashes here.

File details

Details for the file django_relations_iterator-0.0.5-py3-none-any.whl.

File metadata

File hashes

Hashes for django_relations_iterator-0.0.5-py3-none-any.whl
Algorithm Hash digest
SHA256 3f04306f77152bf37c0e3218c8bf655ef9a427b8f8f7406d457e76455e87e616
MD5 d430777ee94e3aa5885f7ab79a2d6cbf
BLAKE2b-256 d84e9218ec63f6b95f0021505ecfb05b514785f170c8c2572eded752c30b5a8b

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page