Skip to content

Instantly share code, notes, and snippets.

@abirafdirp
Last active September 20, 2017 06:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save abirafdirp/73d5d8205272a046a909ab17d451b317 to your computer and use it in GitHub Desktop.
Save abirafdirp/73d5d8205272a046a909ab17d451b317 to your computer and use it in GitHub Desktop.
My implementation of tree structure in Django models
import logging
import os
from django.db import models
from django.utils import timezone
from autoslug import AutoSlugField
from ckeditor.fields import RichTextField
from positions.fields import PositionField
from slugify import slugify
from keepinview.users.models import User
from keepinview.utility.fields import S3PrivateFileField
class TimeStampedModel(models.Model):
created = models.DateTimeField('Date created', auto_now_add=True)
updated = models.DateTimeField('Date updated', auto_now=True)
class Meta:
abstract = True
ordering = ('-created', )
class Topic(TimeStampedModel):
title = models.CharField(max_length=100, unique=True)
content = RichTextField(default='', blank=True, null=True)
slug = AutoSlugField(populate_from='title', always_update=True)
def generate_list_of_cloned_topics(self):
return ', '.join(
[cloned_topic.__str__()
for cloned_topic
in self.cloned_topics.all()]
)
def save(self, *args, **kwargs):
# because the title will be primarily displayed in HTML,
# using multiple whitespaces between words breaks the report generation
#
# this is due to the difference between the displayed title
# (only one whitespace maximum)
# and the internal value (might be multiple whitespaces)
# so PDF can't be parsed properly
self.title = ' '.join(self.title.split())
return super(Topic, self).save(*args, **kwargs)
def __str__(self):
return '{}'.format(self.title)
# Put at the main body for Python 2 compatibility sigh
def get_topic_title(instance):
return instance.topic.title
class ClonedTopic(TimeStampedModel):
topic = models.ForeignKey(Topic, related_name='cloned_topics')
# Not human friendly, needs to be added 1 in the frontend
position = PositionField(collection='parent')
slug = AutoSlugField(
populate_from=get_topic_title,
always_update=True,
unique_with=('topic', 'parent')
)
parent = models.ForeignKey('self', blank=True, null=True,
related_name='children')
# Notation string no longer exists because PositionField does not call
# save method on update. This makes notation string inconsistent.
# Because fancytree already have its own notation string generator.
# notation_string = models.CharField(max_length=30, editable=False)
def generate_notation_string(self):
if self.parent:
return '{}.{}'.format(
self.parent.generate_notation_string(),
self.position + 1
)
else:
return str(self.position + 1)
def get_number_of_parents(self):
if self.parent:
return self.generate_notation_string().count('.')
else:
return 0
def generate_list_of_children(self):
"""
Generate list of children of this Cloned Topic
"""
topic_titles = [
cloned_topic.topic.title
for cloned_topic
in self.children.all()
]
return topic_titles
@staticmethod
def generate_list_of_all_children(cloned_topic, first=True, result=None):
"""
Generate all children e.g. grandchildren will also be included.
Do not supply result and first with an argument.
"""
if first:
first = False
if result is None:
result = []
if cloned_topic.children.all():
for children in cloned_topic.children.all():
result.append(children)
cloned_topic.generate_list_of_all_children(
cloned_topic=children,
first=False,
result=result
)
return result
@classmethod
def do_healthcheck(cls, parent):
"""
Sometime back in the development phase, I encountered some cloned
topics position will get messed up, e.g no position 3 in the same level
, although that should not be happened, but because I was having a hard
time to reproduce it, so I just make this function.
cloned topics that will get health checked is the children of the
parent argument.
:param parent: parent of the cloned topics, the children will get
health checked
:return: None
"""
cloned_topics = cls.objects.filter(parent=parent).order_by('position')
for i, cloned_topic in enumerate(cloned_topics):
if cloned_topic.position != i:
logging.warning('bad positions corrected')
cloned_topic.position = i
cloned_topic.save()
def __str__(self):
return '{} {}'.format(
self.generate_notation_string(),
get_topic_title(self)
)
class Meta:
ordering = ('parent__position', 'position')
class Note(TimeStampedModel):
# Title is optional so it's easy to just create a note with only the
# content, ID will be used as the identifier in the frontend
title = models.CharField(max_length=100, default='', blank=True, null=True)
content = RichTextField(default='', blank=True, null=True)
# Act as OnetoOne field in the frontend, so I added unique True here
# ForeignKey is preserved as requested in the issue #19
cloned_topic = models.ForeignKey(ClonedTopic, related_name='notes',
unique=True)
# Because for now cloned_topic is acted as OneToOne, populate from
# is using that field
slug = AutoSlugField(populate_from='cloned_topic', always_update=True)
def __str__(self):
return self.title
def add_date_created(instance, filename):
if instance.topic:
title_slug = slugify(instance.topic.title)
path = os.path.join('topics', title_slug, filename)
else:
# when manually created in admin without a topic,
# external documents that get its topic deleted will handled in signals
path = os.path.join('topics', filename)
return path
class ExternalDocument(TimeStampedModel):
topic = models.ForeignKey(Topic, related_name='external_documents',
null=True, blank=True, on_delete=models.SET_NULL)
name = models.CharField(max_length=255, blank=True, null=True)
file = S3PrivateFileField(upload_to=add_date_created, max_length=500)
slug = AutoSlugField(populate_from='name', unique_with=('name', 'file'),
always_update=True)
def save(self, *args, **kwargs):
if not self.name:
self.name = self.file.name
return super(ExternalDocument, self).save(*args, **kwargs)
def __str__(self):
return '{}'.format(self.name)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment