GithubHelp home page GithubHelp logo

fabiocaccamo / django-treenode Goto Github PK

View Code? Open in Web Editor NEW
640.0 14.0 31.0 404 KB

:deciduous_tree: probably the best abstract model/admin for your tree based stuff.

License: MIT License

Python 91.32% CSS 1.51% JavaScript 7.17%
django tree node categories abstract model python category trees nodes

django-treenode's Introduction

django-treenode

Probably the best abstract model / admin for your tree based stuff.

Features

  • Fast - get ancestors, children, descendants, parent, root, siblings, tree with no queries
  • Synced - in-memory model instances are automatically updated
  • Compatibility - you can easily add treenode to existing projects
  • No dependencies
  • Easy configuration - just extend the abstract model / model-admin
  • Admin integration - great tree visualization: accordion, breadcrumbs or indentation
indentation (default) breadcrumbs accordion
treenode-admin-display-mode-indentation treenode-admin-display-mode-breadcrumbs treenode-admin-display-mode-accordion

Installation

  • Run pip install django-treenode
  • Add treenode to settings.INSTALLED_APPS
  • Make your model inherit from treenode.models.TreeNodeModel (described below)
  • Make your model-admin inherit from treenode.admin.TreeNodeModelAdmin (described below)
  • Run python manage.py makemigrations and python manage.py migrate

Configuration

models.py

Make your model class inherit from treenode.models.TreeNodeModel:

from django.db import models

from treenode.models import TreeNodeModel


class Category(TreeNodeModel):

    # the field used to display the model instance
    # default value 'pk'
    treenode_display_field = "name"

    name = models.CharField(max_length=50)

    class Meta(TreeNodeModel.Meta):
        verbose_name = "Category"
        verbose_name_plural = "Categories"

The TreeNodeModel abstract class adds many fields (prefixed with tn_ to prevent direct access) and public methods to your models.

⚠️ If you are extending a model that already has some fields, please ensure that your model existing fields names don't clash with TreeNodeModel public methods/properties names.


admin.py

Make your model-admin class inherit from treenode.admin.TreeNodeModelAdmin.

from django.contrib import admin

from treenode.admin import TreeNodeModelAdmin
from treenode.forms import TreeNodeForm

from .models import Category


class CategoryAdmin(TreeNodeModelAdmin):

    # set the changelist display mode: 'accordion', 'breadcrumbs' or 'indentation' (default)
    # when changelist results are filtered by a querystring,
    # 'breadcrumbs' mode will be used (to preserve data display integrity)
    treenode_display_mode = TreeNodeModelAdmin.TREENODE_DISPLAY_MODE_ACCORDION
    # treenode_display_mode = TreeNodeModelAdmin.TREENODE_DISPLAY_MODE_BREADCRUMBS
    # treenode_display_mode = TreeNodeModelAdmin.TREENODE_DISPLAY_MODE_INDENTATION

    # use TreeNodeForm to automatically exclude invalid parent choices
    form = TreeNodeForm

admin.site.register(Category, CategoryAdmin)

settings.py

You can use a custom cache backend by adding a treenode entry to settings.CACHES, otherwise the default cache backend will be used.

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
        "LOCATION": "...",
    },
    "treenode": {
        "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
    },
}

Usage

Methods/Properties

delete

Delete a node if cascade=True (default behaviour), children and descendants will be deleted too, otherwise children's parent will be set to None (then children become roots):

obj.delete(cascade=True)

delete_tree

Delete the whole tree for the current node class:

cls.delete_tree()

get_ancestors

Get a list with all ancestors (ordered from root to parent):

obj.get_ancestors()
# or
obj.ancestors

get_ancestors_count

Get the ancestors count:

obj.get_ancestors_count()
# or
obj.ancestors_count

get_ancestors_pks

Get the ancestors pks list:

obj.get_ancestors_pks()
# or
obj.ancestors_pks

get_ancestors_queryset

Get the ancestors queryset (ordered from parent to root):

obj.get_ancestors_queryset()

get_breadcrumbs

Get the breadcrumbs to current node (included):

obj.get_breadcrumbs(attr=None)
# or
obj.breadcrumbs

get_children

Get a list containing all children:

obj.get_children()
# or
obj.children

get_children_count

Get the children count:

obj.get_children_count()
# or
obj.children_count

get_children_pks

Get the children pks list:

obj.get_children_pks()
# or
obj.children_pks

get_children_queryset

Get the children queryset:

obj.get_children_queryset()

get_depth

Get the node depth (how many levels of descendants):

obj.get_depth()
# or
obj.depth

get_descendants

Get a list containing all descendants:

obj.get_descendants()
# or
obj.descendants

get_descendants_count

Get the descendants count:

obj.get_descendants_count()
# or
obj.descendants_count

get_descendants_pks

Get the descendants pks list:

obj.get_descendants_pks()
# or
obj.descendants_pks

get_descendants_queryset

Get the descendants queryset:

obj.get_descendants_queryset()

get_descendants_tree

Get a n-dimensional dict representing the model tree:

obj.get_descendants_tree()
# or
obj.descendants_tree

get_descendants_tree_display

Get a multiline string representing the model tree:

obj.get_descendants_tree_display()
# or
obj.descendants_tree_display

get_first_child

Get the first child node:

obj.get_first_child()
# or
obj.first_child

get_index

Get the node index (index in node.parent.children list):

obj.get_index()
# or
obj.index

get_last_child

Get the last child node:

obj.get_last_child()
# or
obj.last_child

get_level

Get the node level (starting from 1):

obj.get_level()
# or
obj.level

get_order

Get the order value used for ordering:

obj.get_order()
# or
obj.order

get_parent

Get the parent node:

obj.get_parent()
# or
obj.parent

get_parent_pk

Get the parent node pk:

obj.get_parent_pk()
# or
obj.parent_pk

set_parent

Set the parent node:

obj.set_parent(parent_obj)

get_priority

Get the node priority:

obj.get_priority()
# or
obj.priority

set_priority

Set the node priority:

obj.set_priority(100)

get_root

Get the root node for the current node:

obj.get_root()
# or
obj.root

get_root_pk

Get the root node pk for the current node:

obj.get_root_pk()
# or
obj.root_pk

get_roots

Get a list with all root nodes:

cls.get_roots()
# or
cls.roots

get_roots_queryset

Get root nodes queryset:

cls.get_roots_queryset()

get_siblings

Get a list with all the siblings:

obj.get_siblings()
# or
obj.siblings

get_siblings_count

Get the siblings count:

obj.get_siblings_count()
# or
obj.siblings_count

get_siblings_pks

Get the siblings pks list:

obj.get_siblings_pks()
# or
obj.siblings_pks

get_siblings_queryset

Get the siblings queryset:

obj.get_siblings_queryset()

get_tree

Get a n-dimensional dict representing the model tree:

cls.get_tree()
# or
cls.tree

get_tree_display

Get a multiline string representing the model tree:

cls.get_tree_display()
# or
cls.tree_display

is_ancestor_of

Return True if the current node is ancestor of target_obj:

obj.is_ancestor_of(target_obj)

is_child_of

Return True if the current node is child of target_obj:

obj.is_child_of(target_obj)

is_descendant_of

Return True if the current node is descendant of target_obj:

obj.is_descendant_of(target_obj)

is_first_child

Return True if the current node is the first child:

obj.is_first_child()

is_last_child

Return True if the current node is the last child:

obj.is_last_child()

is_leaf

Return True if the current node is leaf (it has not children):

obj.is_leaf()

is_parent_of

Return True if the current node is parent of target_obj:

obj.is_parent_of(target_obj)

is_root

Return True if the current node is root:

obj.is_root()

is_root_of

Return True if the current node is root of target_obj:

obj.is_root_of(target_obj)

is_sibling_of

Return True if the current node is sibling of target_obj:

obj.is_sibling_of(target_obj)

update_tree

Update tree manually, useful after bulk updates:

cls.update_tree()

Bulk Operations

To perform bulk operations it is recommended to turn off signals, then triggering the tree update at the end:

from treenode.signals import no_signals

with no_signals():
    # execute custom bulk operations
    pass

# trigger tree update only once
YourModel.update_tree()

FAQ

Custom tree serialization

How can I serialize a tree using a custom data structure?

This has been discussed here.

Testing

# clone repository
git clone https://github.com/fabiocaccamo/django-treenode.git && cd django-treenode

# create virtualenv and activate it
python -m venv venv && . venv/bin/activate

# upgrade pip
python -m pip install --upgrade pip

# install requirements
pip install -r requirements.txt -r requirements-test.txt

# install pre-commit to run formatters and linters
pre-commit install --install-hooks

# run tests
tox
# or
python runtests.py
# or
python -m django test --settings "tests.settings"

License

Released under MIT License.


Supporting

See also

  • django-admin-interface - the default admin interface made customizable by the admin itself. popup windows replaced by modals. 🧙 ⚡

  • django-cache-cleaner - clear the entire cache or individual caches easily using the admin panel or management command. 🧹✨

  • django-colorfield - simple color field for models with a nice color-picker in the admin. 🎨

  • django-extra-settings - config and manage typed extra settings using just the django admin. ⚙️

  • django-maintenance-mode - shows a 503 error page when maintenance-mode is on. 🚧 🛠️

  • django-redirects - redirects with full control. ↪️

  • python-benedict - dict subclass with keylist/keypath support, I/O shortcuts (base64, csv, json, pickle, plist, query-string, toml, xml, yaml) and many utilities. 📘

  • python-codicefiscale - encode/decode Italian fiscal codes - codifica/decodifica del Codice Fiscale. 🇮🇹 💳

  • python-fontbro - friendly font operations. 🧢

  • python-fsutil - file-system utilities for lazy devs. 🧟‍♂️

django-treenode's People

Contributors

cperrin88 avatar dependabot[bot] avatar fabiocaccamo avatar github-actions[bot] avatar jvacek avatar nathan-cohen avatar pre-commit-ci[bot] avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

django-treenode's Issues

django dataload > 'NoneType' object has no attribute 'get_parent_pk'

Python version
3.11

Django version
4.1.7

Package version
0.19.0

Current behavior (bug description)

With Simple Model with no relation

class MyModel(models.Model, TreeNodeModel):
    ...
    name: str = models.CharField(max_length=50)
    treenode_display_field = "name"
    ...

After some inserts with parentals relations, then a django dumpdata with this model only :

python manage.py dumpdata MyModel --output=my_model.json

on a standart django dataload :

python manage.py loaddata my_model
> AttributeError: Problem installing fixture 'my_model.json': 'NoneType' object has no attribute 'get_parent_pk'

Expected behavior

dataload OK

Fund with Polar

[Question] Can I use this library to represent related models?

I have the following models:

  • Structure
  • StructureSeries
  • StructureSeriesSlot

The model Structure has many StructureSeries. In turn, StructureSeries has many StructureSeriesSlot.

I noticed your example covers only for the records within the same model class to be treenodes within that model class.

Does it work for related models (related via ForeignKey Field)?

remove a node in tree without deleting descendants

It would be nice to allow for behaviour where deleting a node would create separate disjoint trees. Any suggestions how this could be achieved with the current implementations? I previously had just an FK field to parent node, which had a on_delete=SET_NULL which worked fine for me, and I would like to replicate this behaviour here.

django-fast-treenode

@fabiocaccamo , have you seen django-fast-treenode ?

I suggested that perhaps the changes in django-fast-treenode should be contributed to this package rather than maintained separately. What do you think?

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

If an object has no parent, admin will throw an exception

I made a super simple, new Django app, and only added this package to test the admin.

Exception Type: AttributeError at /admin/tree/category/
Exception Value: 'Category' object has no attribute 'tn_parents_count'

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

bulk create / update ?

hi,

do you plan to provide the bulk_create/update methods on TreeNodeModel ?

for now I do a manual bulk_update specifying all the fields.

regards,
Jérémy

Search for subnodes in accordion admin isn't possible

Search for subnodes in accordion admin isn't possible, because the parent nodes are collapsed and can not be opened.

Model:
class Element(TreeNodeModel): TYPES = Choices('Namespace', 'Class', 'Object') name = models.CharField(max_length=255, blank=False, null=False, unique=True) model_type = models.CharField( choices=TYPES, default=TYPES.Object, max_length=100) treenode_display_field = 'name'

Admin:
@admin.register(models.Element) class ElementAdmin(TreeNodeModelAdmin): # admin.ModelAdmin treenode_accordion = True autocomplete_fields = ['tn_parent'] list_display = ('id', 'model_type', ) search_fields = ['name', 'tn_parent__name'] exclude = ('tn_priority', ) form = TreeNodeForm

No search:
grafik

Search for child node:
grafik

UUID primary keys don't work

Python version
3.9

Django version
3.2.4

Package version
0.16.0

Current behavior (bug description)
If the primary key of a model is a UUID field the plugin doesn't work.

Traceback

Traceback (most recent call last):
  File "<venv>\lib\site-packages\django\core\handlers\exception.py", line 47, in inner
    response = get_response(request)
  File "<venv>\lib\site-packages\django\core\handlers\base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "<venv>\lib\site-packages\django\contrib\admin\options.py", line 616, in wrapper
    return self.admin_site.admin_view(view)(*args, **kwargs)
  File "<venv>\lib\site-packages\django\utils\decorators.py", line 130, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "<venv>\lib\site-packages\django\views\decorators\cache.py", line 44, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "<venv>\lib\site-packages\django\contrib\admin\sites.py", line 232, in inner
    return view(request, *args, **kwargs)
  File "<venv>\lib\site-packages\django\contrib\admin\options.py", line 1660, in change_view
    return self.changeform_view(request, object_id, form_url, extra_context)
  File "<venv>\lib\site-packages\django\utils\decorators.py", line 43, in _wrapper
    return bound_method(*args, **kwargs)
  File "<venv>\lib\site-packages\django\utils\decorators.py", line 130, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "<venv>\lib\site-packages\django\contrib\admin\options.py", line 1540, in changeform_view
    return self._changeform_view(request, object_id, form_url, extra_context)
  File "<venv>\lib\site-packages\django\contrib\admin\options.py", line 1586, in _changeform_view
    self.save_model(request, new_object, form, not add)
  File "<venv>\lib\site-packages\django\contrib\admin\options.py", line 1099, in save_model
    obj.save()
  File "<venv>\lib\site-packages\django\db\models\base.py", line 726, in save
    self.save_base(using=using, force_insert=force_insert,
  File "<venv>\lib\site-packages\django\db\models\base.py", line 774, in save_base
    post_save.send(
  File "<venv>\lib\site-packages\django\dispatch\dispatcher.py", line 180, in send
    return [
  File "<venv>\lib\site-packages\django\dispatch\dispatcher.py", line 181, in <listcomp>
    (receiver, receiver(signal=self, sender=sender, **named))
  File "<venv>\lib\site-packages\treenode\signals.py", line 36, in post_save_treenode
    sender.update_tree()
  File "<venv>\lib\site-packages\treenode\models.py", line 371, in update_tree
    objs_data = cls.__get_nodes_data()
  File "<venv>\lib\site-packages\treenode\models.py", line 446, in __get_nodes_data
    objs_data_dict = {str(obj.pk):obj.__get_node_data(objs_list, objs_dict) for obj in objs_list}
  File "<venv>\lib\site-packages\treenode\models.py", line 446, in <dictcomp>
    objs_data_dict = {str(obj.pk):obj.__get_node_data(objs_list, objs_dict) for obj in objs_list}
  File "<venv>\lib\site-packages\treenode\models.py", line 417, in __get_node_data
    order_strs = [obj.__get_node_order_str() for obj in order_objs]
  File "<venv>\lib\site-packages\treenode\models.py", line 417, in <listcomp>
    order_strs = [obj.__get_node_order_str() for obj in order_objs]
  File "<venv>\lib\site-packages\treenode\models.py", line 394, in __get_node_order_str
    pk_val = min(self.pk, priority_max)
TypeError: '<' not supported between instances of 'int' and 'UUID'

Ordering Problem with recursive method

Hi!

I've a problem when execute TreeNode Model with nested list, the Tree isn´t respect the order iteration, for example:

FAKE_PAYLOAD = [
    { 'name': "LEGAL",
      'children': [{ 'name': "CORPORATIVO",
                     'children': [{ 'name': "Sub-SubFolder 1" },
                                  { 'name': "Sub-SubFolder 2" },
                                  { 'name': "Sub-SubFolder 3" },
                                  { 'name': "Sub-SubFolder 4" },
                                  { 'name': "Sub-SubFolder 5" },
                                  { 'name': "Sub-SubFolder 6" },
                                  { 'name': "Sub-SubFolder 7" },
                                  { 'name': "Sub-SubFolder 8" },
                                  { 'name': "Sub-SubFolder 9" },
                                  { 'name': "Sub-SubFolder 10" },
                                  { 'name': "Sub-SubFolder 11" },
                                  { 'name': "Sub-SubFolder 12" },
                                  { 'name': "Sub-SubFolder 13" },
                                  { 'name': "Sub-SubFolder 14" },
                                  { 'name': "Sub-SubFolder 15" },
                                  ] },
                   { 'name': "CONTRACTUAL",
                     'children': [
                         { 'name': "Sub-SubFolder 1" },
                         { 'name': "Sub-SubFolder 2" }] },
                   { 'name': "GOBIERNO",
                     'children': [{ 'name': "Sub-SubFolder 1" }] }
                   ] },
]

My model is like this:

class FakeFolder(TreeNodeModel):
    name = models.CharField(max_length=255)

    def __str__(self) -> str:
        return self.name

My recursive method is:

def write_tree(tree_list, parent = None):
    for folder in tree_list:
        folder_obj = FakeFolder.objects.create(name=folder['name'])
        if parent:
            folder_obj.set_parent(parent)
        if 'children' in folder:
            write_tree(folder['children'], folder_obj)

After, executed get_children() of root node, the list it seems wrong order:

FakeFolder.objects.all()[0].children
[<FakeFolder: CONTRACTUAL>, <FakeFolder: CORPORATIVO>, <FakeFolder: GOBIERNO>]

And the order of children_pks is '106,90,109', when the correct order must be maybe '90,106,109' or:

[<FakeFolder: CORPORATIVO>, <FakeFolder: CONTRACTUAL>, <FakeFolder: GOBIERNO>]

With this package it's possible to set order manually? I don't see a method for this. The only way that I've found is with the priority property but only works in descendent order

Thanks!

Implement closure-tree to improve performance.

Hello!
I want to share the experience of using the module. Today I see many of its advantages and two main disadvantages. One of them I want to discuss. This is control over the order of elements.

Description of the problem.
The module does not have a transparent and understandable mechanism for ordering elements in the tree. In practice, the most common are two modes: alphabetical sorting and strict order set by the user. The logic dictates that the tn_priority field provided by the default form should do this. If it is set to 0 for all elements, then they must be ordered alphabetically. If it is specified, then the field value determines the order.

But alas, this is not the case.

Another method of establishing a coercive order of elements, which is intuitively prompted by experience, is also not suitable. This is an attempt to import data with the tn_order field set.

It would be nice if you made it easier to manage the order of items in tree.

PS. The main trouble is that with all my attachment to this module, without a mechanism for intelligible order management, I have to refuse to use it. And it just tears my soul to shreds :-(

Funding

  • You can sponsor this specific effort via a Polar.sh pledge below
  • We receive the pledge once the issue is completed & verified
Fund with Polar

String PK support

Hello,

Currently, trying to compare string primary keys will fail because treenode will try to compare the pk (a string) to a number.

image

Could we please add a support to string primary keys ? :)

Thank you very much !

tn_descendants_count is too small for big trees

Python version
3.9

Django version
3.2

Package version
0.18.0

Current behavior (bug description)
since the fix of #44, a new exception is raised during my big tree building.

Expected behavior
use treenode for my ~ 100k nodes tree.

Comments

Thank you so much! You have done a great job and created a very good product. At the same time, it should be noted that its main drawback is scarce documentation. It would be nice if you could take a little time and add some small comments to the code.
It's just that I, for example, spend a lot of time to understand what this or that parameter does. For example, I still did not understand what is node priority?
Tnx!

Add custom cache backend support

Appeared first on issue #19 by @aarondiazr

I see behind of magic performance one weakness, for example if execute this on microservice or serveless, will always go to database because the memory location of cache is independently of any instance of docker, maybe a feature with redis will be great.

remove verbose logging in django console

it would be nice if the verbose logging in the django console could be removed, i.e. I get the following output for each added item:

[treenode] update acme.models.organization tree: executed 0 queries in 0.4567100000s.
 .
 .
 .
[treenode] update acme.models.organization tree: executed 0 queries in 0.589100000s.

How to use methods like `set_parent` during manual RunPython in migrations?

Python version
3.9.1

Django version
2.2.24

Package version
0.17.0

Current behaviour (bug description)
My model was previously using the MPTT library, and so I'd like to transfer to TreeNode. However I am not really able to do the migration due to the methods not being available when using the apps.get_model tactic, and I think directly working on the keys might not be a good idea seeing as set_parent has things going on.

def migrate_mptt_treenode(apps, schema_editor):
    MyModel = apps.get_model("myapp", "MyModel")
    for c in MyModel.objects.all():
        if c.parent is not None:
            c.set_parent(c.parent)
            c.save()

The info about the parent was stored in parent before, which is a TreeForeignKey Field from MPTT. I want to first migrate in the new fields from treenode, make the migration, and then remove the inheritance for the MPTTModel after the data is moved.

Expected behaviour
I know that apps.get_model doesn't make the methods available, so it's not that this should work. Some alternatives for this use-case would be helpful though.

At the 6th level, treenode reverses the order of descendants.

Python version
3.8.0

Django version
2.2.7

Package version
django-treenode: 0.14.0

Current behavior (bug description)
Here is a single branch with 5 nodes:
image

When I add the 6th descendant node to the branch, the order of descendants begins to reverse as seen below:
image

When I add the 7th and 8th descendant, it follows the same reverse behavior:
image

Here's a snapshot of the table:
image

Am I doing something wrong?

Expected behavior
?

Thank you!

Queries getting slower during progress

Thank you for providing this nice library. I am using it in a scientific project for twitter analysis. However, the db inserts seem to slow down a lot after a couple of days running it in production mode:

[treenode] update delab.models.Tweet tree: executed 0 queries in 71.44206510693766s.
[treenode] update delab.models.Tweet tree: executed 0 queries in 71.87405565101653s.
[treenode] update delab.models.Tweet tree: executed 0 queries in 66.6588648010511s.
[treenode] update delab.models.Tweet tree: executed 0 queries in 71.47152532404289s.
[treenode] update delab.models.Tweet tree: executed 0 queries in 79.63660701399203s.

I opened the issue also within my project, if you are interested in the way the library is used:
juliandehne/delab#15

Probably, I will write a unit test to verify it is an issue with the treenode library.

Any ideas?

Fund with Polar

treenode's sorting is not OK

Hi,

i reproduce a directories treenode with name's sorting and in a directory viewer the sorting seem to be OK but with treenode, it's not OK.
It seem to me that there is a problem with letter and number.

see in attachments 2 screenshots, one to see the directory and the other to see in the treenode directory.

image

image

here is my pip list output :
backports.csv (1.0.7)
defusedxml (0.6.0)
diff-match-patch (20181111)
Django (2.1)
django-debug-toolbar (1.11)
django-extensions (2.1.7)
django-import-export (1.2.0)
django-js-asset (1.2.2)
django-treenode (0.13.1)
et-xmlfile (1.0.1)
jdcal (1.4.1)
odfpy (1.4.0)
openpyxl (2.6.2)
pip (9.0.1)
pkg-resources (0.0.0)
psycopg2-binary (2.8.2)
pydotplus (2.0.2)
pyparsing (2.4.0)
pytz (2019.1)
PyYAML (5.1)
setuptools (32.3.1)
six (1.12.0)
sqlparse (0.3.0)
tablib (0.13.0)
wheel (0.33.4)
xlrd (1.2.0)
xlwt (1.3.0)

.update_tree() error value too long for type character varying(500)

treenode tries to set tn_siblings_pks and its could be longer than 500 charts. And thast only with 157 records in my table, where all of them root level

update data

{'tn_siblings_pks': '2,5,46,18,62,142,158,13,40,11,135,51,23,66,6,74,31,98,17,104,19,118,105,83,30,90,15,56,137,54,96,125,148,58,26,50,89,35,29,108,36,86,153,57,87,25,78,84,77,20,85,41,45,44,81,73,109,150,92,37,12,110,48,151,99,133,43,128,103,146,61,156,65,32,155,131,141,42,75,97,34,119,55,64,95,116,147,113,157,106,59,28,126,144,139,132,60,152,9,136,7,107,134,80,33,101,114,121,88,120,49,145,123,22,63,127,79,21,138,24,100,14,129,143,4,3,93,140,154,16,27,47,68,69,71,72,76,82,91,111,67,117,149,10,122,8,53,52,94,124,70,112,39,102,115,130,38', 'tn_siblings_count': 157, 'tn_order': 117, 'tn_index': 117}
Traceback (most recent call last):
  File "C:\Dev\PycharmProjects\rasimplefront\ra-front-simple-nu\back\venv\lib\site-packages\django\db\backends\utils.py", line 85, in _execute
    return self.cursor.execute(sql, params)
psycopg2.DataError: value too long for type character varying(500)
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  File "C:\Python36\lib\code.py", line 91, in runcode
    exec(code, self.locals)
  File "<input>", line 1, in <module>
  File "C:\Dev\PycharmProjects\rasimplefront\ra-front-simple-nu\back\venv\lib\site-packages\treenode\models.py", line 367, in update_tree
    cls.objects.filter(pk=obj_pk).update(**obj_data)
  File "C:\Dev\PycharmProjects\rasimplefront\ra-front-simple-nu\back\venv\lib\site-packages\django\db\models\query.py", line 693, in update
    rows = query.get_compiler(self.db).execute_sql(CURSOR)
  File "C:\Dev\PycharmProjects\rasimplefront\ra-front-simple-nu\back\venv\lib\site-packages\django\db\models\sql\compiler.py", line 1383, in execute_sql
    cursor = super().execute_sql(result_type)
  File "C:\Dev\PycharmProjects\rasimplefront\ra-front-simple-nu\back\venv\lib\site-packages\django\db\models\sql\compiler.py", line 1065, in execute_sql
    cursor.execute(sql, params)
  File "C:\Dev\PycharmProjects\rasimplefront\ra-front-simple-nu\back\venv\lib\site-packages\django\db\backends\utils.py", line 100, in execute
    return super().execute(sql, params)
  File "C:\Dev\PycharmProjects\rasimplefront\ra-front-simple-nu\back\venv\lib\site-packages\raven\contrib\django\client.py", line 127, in execute
    return real_execute(self, sql, params)
  File "C:\Dev\PycharmProjects\rasimplefront\ra-front-simple-nu\back\venv\lib\site-packages\django\db\backends\utils.py", line 68, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "C:\Dev\PycharmProjects\rasimplefront\ra-front-simple-nu\back\venv\lib\site-packages\django\db\backends\utils.py", line 77, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "C:\Dev\PycharmProjects\rasimplefront\ra-front-simple-nu\back\venv\lib\site-packages\django\db\backends\utils.py", line 85, in _execute
    return self.cursor.execute(sql, params)
  File "C:\Dev\PycharmProjects\rasimplefront\ra-front-simple-nu\back\venv\lib\site-packages\django\db\utils.py", line 89, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "C:\Dev\PycharmProjects\rasimplefront\ra-front-simple-nu\back\venv\lib\site-packages\django\db\backends\utils.py", line 85, in _execute
    return self.cursor.execute(sql, params)
django.db.utils.DataError: value too long for type character varying(500)

that kind of make lib unusable

Database Configuration error in update_tree

Hello,

I'm contacting you to report a database configuration problem in the update_tree method of the TreeNodeModel class, which causes the following error:
settings.DATABASES is improperly configured. Please supply the ENGINE value. Check settings documentation for more details.

The method concerned is as follows:

def update_tree(cls):
        debug_message_prefix = (
            f"[treenode] update {cls.__module__}.{cls.__name__} tree: "
        )

        with debug_performance(debug_message_prefix):
            # update db
            objs_data = cls.__get_nodes_data()

            with transaction.atomic():   # <- Here lies the problem
                obj_manager = cls.objects
                for obj_pk, obj_data in objs_data.items():
                    obj_manager.filter(pk=obj_pk).update(**obj_data)

            # update in-memory instances
            update_refs(cls, objs_data)

            # update cache instances
            update_cache(cls)

I suggest solving this problem by modifying the with transaction.atomic(): block as follows :
with transaction.atomic(using=router.db_for_write(AccessAttempt)):

The final result would be :

def update_tree(cls):
        debug_message_prefix = (
            f"[treenode] update {cls.__module__}.{cls.__name__} tree: "
        )

        with debug_performance(debug_message_prefix):
            # update db
            objs_data = cls.__get_nodes_data()

            with transaction.atomic(using=router.db_for_write(AccessAttempt)): # <- Correction applied here
                obj_manager = cls.objects
                for obj_pk, obj_data in objs_data.items():
                    obj_manager.filter(pk=obj_pk).update(**obj_data)

            # update in-memory instances
            update_refs(cls, objs_data)

            # update cache instances
            update_cache(cls)

Correction Proposal

  • i am willing to create a Pull Request to make this correction if you find it appropriate. I'd be happy to help solve this problem and improve this project.

  • By explicitly specifying using=router.db_for_write(AccessAttempt) when using transaction.atomic(), we make it clear to Django which database router should be used for this particular operation. This can be crucial in environments where there are multiple database configurations, to ensure that write operations are directed to the correct database that supports writing, thus avoiding potential problems associated with attempts to write to a read-only database.

Additional details

Affected class: TreeNodeModel
Relevant method: update_tree
Affected file: /treenode/models.py
I thank you in advance for your time and consideration and remain available to discuss this issue further if needed.

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

List of reserved field names

I think it would be helpful to have list of reserved field names that should not be used, for example 'level' as I just discovered. Thanks for the library. Keep up the great work.

Admin issue

There a few errors in the changelist_view

AttributeError: 'TestTreeNode' object has no attribute 'tn_parents_pks'

And

AttributeError: 'TestTreeNode' object has no attribute 'tn_parents_count'

I suppose ancestors should be used instead of parents

Order is too small for big trees

Hi,

Python version
3.9

Django version
3.2

Package version
0.17.0

Current behavior (bug description)
I try to store unix FS trees in PostgreSQL using django-treenode. The tree contains around 77000 files.
All the files are loaded flatten into db, then I do some batch update to rebuild the tree.

During the call to update_tree(), the DB update raises a "smallint out of range" error. It seems to be the order field that is out of range:
[...]"tn_depth" = 1, "tn_level" = 4, "tn_order" = 33673, "tn_index" = 6 WHERE[...]

I tried to force the order to 0, but it did not help.

Expected behavior
update_tree() is working on large trees.

Performance issues

Been trying to create a few thousand items without any hierarchy infos, like this:


# models.py

class Customer(TreeNodeModel):

    treenode_display_field = 'name'
    name = models.CharField(max_length=50)

    class Meta(TreeNodeModel.Meta):
        verbose_name = 'Customer'
        verbose_name_plural = 'Customer'

# script.py

for x in names:
    obj, created = Customer.objects.get_or_create(name=x)

But the DB becomes incredibly slow after a couple hundred rows eg

[treenode] update bpids.models.Customer tree: executed 0 queries in 1.9379002410000226s.
[treenode] update bpids.models.Customer tree: executed 0 queries in 1.9850395860000276s.
[treenode] update bpids.models.Customer tree: executed 0 queries in 2.2034165819999885s.
[treenode] update bpids.models.Customer tree: executed 0 queries in 2.5427609600000096s.
[treenode] update bpids.models.Customer tree: executed 0 queries in 2.6210373019999906s.
[treenode] update bpids.models.Customer tree: executed 0 queries in 3.163530485000024s.
[treenode] update bpids.models.Customer tree: executed 0 queries in 1.538068753999994s.

So totally unusable...

Anything I'm doing wrong?

on emit_post_migrate_signal sender_model.update_tree() raises an exception when used in multitenancy with django_tenants package

I have two schema types public and other type ..On migrating I get an error as that model is not in public schema but in that other type
Below is the error message


File "/home/dante/anaconda3/envs/memplas/lib/python3.11/site-packages/django/core/management/commands/migrate.py", line 383, in handle
    emit_post_migrate_signal(
  File "/home/dante/anaconda3/envs/memplas/lib/python3.11/site-packages/django/core/management/sql.py", line 52, in emit_post_migrate_signal
    models.signals.post_migrate.send(
  File "/home/dante/anaconda3/envs/memplas/lib/python3.11/site-packages/django/dispatch/dispatcher.py", line 176, in send
    return [
           ^
  File "/home/dante/anaconda3/envs/memplas/lib/python3.11/site-packages/django/dispatch/dispatcher.py", line 177, in <listcomp>
    (receiver, receiver(signal=self, sender=sender, **named))
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/dante/anaconda3/envs/memplas/lib/python3.11/site-packages/treenode/signals.py", line 29, in post_migrate_treenode
    sender_model.update_tree()
  File "/home/dante/anaconda3/envs/memplas/lib/python3.11/site-packages/treenode/models.py", line 425, in update_tree
    objs_data = cls.__get_nodes_data()
                ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/dante/anaconda3/envs/memplas/lib/python3.11/site-packages/treenode/models.py", line 509, in __get_nodes_data
    objs_list = list(objs_qs)
                ^^^^^^^^^^^^^
  File "/home/dante/anaconda3/envs/memplas/lib/python3.11/site-packages/django/db/models/query.py", line 398, in __iter__
    self._fetch_all()
  File "/home/dante/anaconda3/envs/memplas/lib/python3.11/site-packages/django/db/models/query.py", line 1881, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/dante/anaconda3/envs/memplas/lib/python3.11/site-packages/django/db/models/query.py", line 91, in __iter__
    results = compiler.execute_sql(
              ^^^^^^^^^^^^^^^^^^^^^
  File "/home/dante/anaconda3/envs/memplas/lib/python3.11/site-packages/django/db/models/sql/compiler.py", line 1562, in execute_sql
    cursor.execute(sql, params)
  File "/home/dante/anaconda3/envs/memplas/lib/python3.11/site-packages/django/db/backends/utils.py", line 102, in execute
    return super().execute(sql, params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/dante/anaconda3/envs/memplas/lib/python3.11/site-packages/django/db/backends/utils.py", line 67, in execute
    return self._execute_with_wrappers(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/dante/anaconda3/envs/memplas/lib/python3.11/site-packages/django/db/backends/utils.py", line 80, in _execute_with_wrappers
    return executor(sql, params, many, context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/dante/anaconda3/envs/memplas/lib/python3.11/site-packages/django/db/backends/utils.py", line 84, in _execute
    with self.db.wrap_database_errors:
  File "/home/dante/anaconda3/envs/memplas/lib/python3.11/site-packages/django/db/utils.py", line 91, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/home/dante/anaconda3/envs/memplas/lib/python3.11/site-packages/django/db/backends/utils.py", line 89, in _execute
    return self.cursor.execute(sql, params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
django.db.utils.ProgrammingError: relation "company_stakeholder" does not exist
                                                            ^

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

question: migrating from django-mptt to django-treenode

I used some of your work in my projects, and they are fantastic; thank you for your hard work.

I have a question, but I'm hesitant to explore till I know whether there is a history example; I searched and couldn't discover anything informative.

I'm working on a project where I'm using django-mptt, and it's becoming clear that it has constraints that are harming the overall development quality and experience. Is it your understanding that transitioning from mptt to treenode is possible/feasible?

Thank you, Layth.

Cache update error

Python version 3.9
Django version 3.2
Package version 0.16.0

Current behavior (bug description)
I just want to say that the error manifests itself in very exotic circumstances.

  1. I created an abstract model from TreeNodeModel
  2. Dynamically, using the operator type(), I create an instance of the tree model, inheriting the model from the previously created abstract model. Entries in ContentType and Permissions have been created. The model is registered in the apps register.
  3. Dynamically using schema_editor.create_model() tables are created in the database

Now the essence of the error. When trying to add a new entry through the admin site, an error occurs in the cache:
Can't pickle <class 'my_app_name.my_module.my_model'>: attribute lookup my_model on my_app_name.my_module failed

Error tracing:
File "D:\Envs\django\lib\site-packages\django\core\handlers\exception.py", line 47, in inner
response = get_response(request)
File "D:\Envs\django\lib\site-packages\django\core\handlers\base.py", line 181, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "D:\Envs\django\lib\site-packages\django\contrib\admin\options.py", line 616, in wrapper
return self.admin_site.admin_view(view)(*args, **kwargs)
File "D:\Envs\django\lib\site-packages\django\utils\decorators.py", line 130, in _wrapped_view
response = view_func(request, *args, **kwargs)
File "D:\Envs\django\lib\site-packages\django\views\decorators\cache.py", line 44, in _wrapped_view_func
response = view_func(request, *args, **kwargs)
File "D:\Envs\django\lib\site-packages\django\contrib\admin\sites.py", line 232, in inner
return view(request, *args, **kwargs)
File "D:\Envs\django\lib\site-packages\django\contrib\admin\options.py", line 1655, in add_view
return self.changeform_view(request, None, form_url, extra_context)
File "D:\Envs\django\lib\site-packages\django\utils\decorators.py", line 43, in _wrapper
return bound_method(*args, **kwargs)
File "D:\Envs\django\lib\site-packages\django\utils\decorators.py", line 130, in _wrapped_view
response = view_func(request, *args, **kwargs)
File "D:\Envs\django\lib\site-packages\django\contrib\admin\options.py", line 1538, in changeform_view
return self._changeform_view(request, object_id, form_url, extra_context)
File "D:\Envs\django\lib\site-packages\django\contrib\admin\options.py", line 1584, in _changeform_view
self.save_model(request, new_object, form, not add)
File "D:\Envs\django\lib\site-packages\django\contrib\admin\options.py", line 1097, in save_model
obj.save()
File "D:\Envs\django\lib\site-packages\django\db\models\base.py", line 726, in save
self.save_base(using=using, force_insert=force_insert,
File "D:\Envs\django\lib\site-packages\django\db\models\base.py", line 774, in save_base
post_save.send(
File "D:\Envs\django\lib\site-packages\django\dispatch\dispatcher.py", line 180, in send
return [
File "D:\Envs\django\lib\site-packages\django\dispatch\dispatcher.py", line 181, in
(receiver, receiver(signal=self, sender=sender, **named))
File "D:\Envs\django\lib\site-packages\treenode\signals.py", line 36, in post_save_treenode
sender.update_tree()
File "D:\Envs\django\lib\site-packages\treenode\models.py", line 382, in update_tree
update_cache(cls)
File "D:\Envs\django\lib\site-packages\treenode\cache.py", line 61, in update_cache
_set_cached_collections(l, d)
File "D:\Envs\django\lib\site-packages\treenode\cache.py", line 33, in _set_cached_collections
c.set('treenode_list', l)

File "D:\Envs\django\lib\site-packages\django\core\cache\backends\locmem.py", line 56, in set
pickled = pickle.dumps(value, self.pickle_protocol)

I understand that dynamic use of the model was not provided for by you, as well as by the developers of Django. But I am weak in dealing with the cache. Perhaps I missed something when creating the model, registering it. I'm asking for ideas on what could have gone wrong. Models created in this way from standard prototypes work great.

I would appreciate any ideas

Implement treenode in CreateView or ModelForm

I am trying implement treenode without admin, and I was curious to know if you have an example of either a CreateView or a ModelForm. Or have any suggestions. I really like your efforts, amazing job, but I do not want regular users into the admin.

Django-treenode for Django 2.1

Hello, I use your application django-treenode I really like it.
I would like to ask to update it to the version of Django 2.1.
Now it works on versions 2.0.8, and the error appears at 2.1 when the admin panel is opened. The error message is shown below.
Thanks for the help.

Environment:

Request Method: GET
Request URL: http://127.0.0.1:8000/admin/platforms/platforms/

Django Version: 2.1
Python Version: 3.7.0
Installed Applications:
['django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sitemaps',
'debug_toolbar',
'django_cleanup',
'comment',
'users',
'software',
'platforms',
'category',
'main',
'activity',
'markdownify',
'treenode']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware']

Traceback:

File "C:\Users\Work\Desktop\choiceapps\venv\lib\site-packages\django\core\handlers\exception.py" in inner
34. response = get_response(request)

File "C:\Users\Work\Desktop\choiceapps\venv\lib\site-packages\django\core\handlers\base.py" in _get_response
126. response = self.process_exception_by_middleware(e, request)

File "C:\Users\Work\Desktop\choiceapps\venv\lib\site-packages\django\core\handlers\base.py" in _get_response
124. response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "C:\Users\Work\Desktop\choiceapps\venv\lib\site-packages\django\contrib\admin\options.py" in wrapper
607. return self.admin_site.admin_view(view)(*args, **kwargs)

File "C:\Users\Work\Desktop\choiceapps\venv\lib\site-packages\django\utils\decorators.py" in _wrapped_view
142. response = view_func(request, *args, **kwargs)

File "C:\Users\Work\Desktop\choiceapps\venv\lib\site-packages\django\views\decorators\cache.py" in _wrapped_view_func
44. response = view_func(request, *args, **kwargs)

File "C:\Users\Work\Desktop\choiceapps\venv\lib\site-packages\django\contrib\admin\sites.py" in inner
223. return view(request, *args, **kwargs)

File "C:\Users\Work\Desktop\choiceapps\venv\lib\site-packages\django\utils\decorators.py" in _wrapper
45. return bound_method(*args, **kwargs)

File "C:\Users\Work\Desktop\choiceapps\venv\lib\site-packages\django\utils\decorators.py" in _wrapped_view
142. response = view_func(request, *args, **kwargs)

File "C:\Users\Work\Desktop\choiceapps\venv\lib\site-packages\django\contrib\admin\options.py" in changelist_view
1685. cl = self.get_changelist_instance(request)

File "C:\Users\Work\Desktop\choiceapps\venv\lib\site-packages\django\contrib\admin\options.py" in get_changelist_instance
745. sortable_by,

File "C:\Users\Work\Desktop\choiceapps\venv\lib\site-packages\django\contrib\admin\views\main.py" in init
80. self.queryset = self.get_queryset(request)

File "C:\Users\Work\Desktop\choiceapps\venv\lib\site-packages\django\contrib\admin\views\main.py" in get_queryset
359. filters_use_distinct) = self.get_filters(request)

File "C:\Users\Work\Desktop\choiceapps\venv\lib\site-packages\django\contrib\admin\views\main.py" in get_filters
113. for list_filter in self.list_filter:

Exception Type: TypeError at /admin/platforms/platforms/
Exception Value: 'NoneType' object is not iterable

Pagination

I scripted creating about 1000 items into my tree and it's a bit slow to load the admin list display and even more slow if I use a raw_id_field for the parent ID (the popup takes like 20 seconds to load).

Is there a way to enable pagination in the admin panel? It's just dumping the full 1000 I generated on each load.

Suggestion: have `treenode_display_field` use `__str__()` as default fallback

We don't have a name-like field in our model, and our __str__ is a combination of the pk, and some other fields. I had to create this in order to have the Admin work, but it would be nice if this could be handled library-side default.

@property
def name(self):
    return str(self)

Something like "unless the treenode_display_field is provided, try using the class' __str__ if it has one)"

This way you wouldn't have to define treenode_display_field in most cases at all

QUESTION: Tree of different models

Hi,
I'm currently building an application and I want to manage a tree of different models in the django admin console. Like the following:

├── Chapter1
└── Chapter2
    ├── Part1
    ├── Part2

Is this possilbe or is there a better way to achive this in django admin ? :)

Note in docs about thread/multi process safety

The worst problem I've had with treebeard is lack of thread/multi process safety.

It's easily demonstrated by running two processes at the same time that each have a loop that adds nodes to the tree.
(because the key generation + adding nodes is not an atomic operation).

Does django-treenode solve this ?

If it does it would be great to have a note in the README.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.