Last active
September 20, 2017 06:24
-
-
Save abirafdirp/73d5d8205272a046a909ab17d451b317 to your computer and use it in GitHub Desktop.
My implementation of tree structure in Django models
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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