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

Overview

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 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 the whole tree for the current node class:

cls.delete_tree()

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

obj.get_ancestors()
# or
obj.ancestors

Get the ancestors count:

obj.get_ancestors_count()
# or
obj.ancestors_count

Get the ancestors pks list:

obj.get_ancestors_pks()
# or
obj.ancestors_pks

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

obj.get_ancestors_queryset()

Get the breadcrumbs to current node (included):

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

Get a list containing all children:

obj.get_children()
# or
obj.children

Get the children count:

obj.get_children_count()
# or
obj.children_count

Get the children pks list:

obj.get_children_pks()
# or
obj.children_pks

Get the children queryset:

obj.get_children_queryset()

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

obj.get_depth()
# or
obj.depth

Get a list containing all descendants:

obj.get_descendants()
# or
obj.descendants

Get the descendants count:

obj.get_descendants_count()
# or
obj.descendants_count

Get the descendants pks list:

obj.get_descendants_pks()
# or
obj.descendants_pks

Get the descendants queryset:

obj.get_descendants_queryset()

Get a n-dimensional dict representing the model tree:

obj.get_descendants_tree()
# or
obj.descendants_tree

Get a multiline string representing the model tree:

obj.get_descendants_tree_display()
# or
obj.descendants_tree_display

Get the first child node:

obj.get_first_child()
# or
obj.first_child

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

obj.get_index()
# or
obj.index

Get the last child node:

obj.get_last_child()
# or
obj.last_child

Get the node level (starting from 1):

obj.get_level()
# or
obj.level

Get the order value used for ordering:

obj.get_order()
# or
obj.order

Get the parent node:

obj.get_parent()
# or
obj.parent

Get the parent node pk:

obj.get_parent_pk()
# or
obj.parent_pk

Set the parent node:

obj.set_parent(parent_obj)

Get the node priority:

obj.get_priority()
# or
obj.priority

Set the node priority:

obj.set_priority(100)

Get the root node for the current node:

obj.get_root()
# or
obj.root

Get the root node pk for the current node:

obj.get_root_pk()
# or
obj.root_pk

Get a list with all root nodes:

cls.get_roots()
# or
cls.roots

Get root nodes queryset:

cls.get_roots_queryset()

Get a list with all the siblings:

obj.get_siblings()
# or
obj.siblings

Get the siblings count:

obj.get_siblings_count()
# or
obj.siblings_count

Get the siblings pks list:

obj.get_siblings_pks()
# or
obj.siblings_pks

Get the siblings queryset:

obj.get_siblings_queryset()

Get a n-dimensional dict representing the model tree:

cls.get_tree()
# or
cls.tree

Get a multiline string representing the model tree:

cls.get_tree_display()
# or
cls.tree_display

Return True if the current node is ancestor of target_obj:

obj.is_ancestor_of(target_obj)

Return True if the current node is child of target_obj:

obj.is_child_of(target_obj)

Return True if the current node is descendant of target_obj:

obj.is_descendant_of(target_obj)

Return True if the current node is the first child:

obj.is_first_child()

Return True if the current node is the last child:

obj.is_last_child()

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

obj.is_leaf()

Return True if the current node is parent of target_obj:

obj.is_parent_of(target_obj)

Return True if the current node is root:

obj.is_root()

Return True if the current node is root of target_obj:

obj.is_root_of(target_obj)

Return True if the current node is sibling of target_obj:

obj.is_sibling_of(target_obj)

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()

Testing

# create python virtual environment
virtualenv testing_django_treenode

# activate virtualenv
cd testing_django_treenode && . bin/activate

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

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

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

License

Released under MIT License.


See also

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

  • 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. 🧟‍♂️

Comments
  • Note in docs about thread/multi process safety

    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.

    enhancement 
    opened by stuaxo 13
  • Cache update error

    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 Permissionshave 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

    bug 
    opened by TimurKady 9
  • remove a node in tree without deleting descendants

    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.

    enhancement 
    opened by jvacek 7
  • QUESTION: Tree of different models

    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 ? :)

    question 
    opened by JuliusJacobitz 6
  • treenode's sorting  is not OK

    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)

    bug 
    opened by nicolasVaye 6
  • Search for subnodes in accordion admin isn't possible

    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

    enhancement 
    opened by alexbredo 6
  • If an object has no parent, admin will throw an exception

    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'
    
    
    opened by douglance 6
  • How to use methods like `set_parent` during manual RunPython in migrations?

    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.

    question 
    opened by jvacek 5
  • bulk create / update ?

    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

    enhancement 
    opened by jvies 5
  • Strange

    Strange "AttributeError: can't set attribute" while using "index" as field name

    Hi! I noted that django-admin creation page throw an "AttributeError" while using "index" as field name.

    I'm using Python 3.8.5 and Django 3.0.8

    don't know if that can be considered a problem.

    Really nice package anyway 😁😁

    opened by paviano 5
  • "TreeNodeModelAdminInline"

    Inline Mixin to edit children in their parent and added default inline if no inlines are defined.

    Either use the TreeNodeModelAdminInline as a MixIn for customizing the inline or just leave it with the default.

    opened by domlysi 5
  • question: migrating from django-mptt to django-treenode

    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.

    enhancement question 
    opened by laith43d 2
  • Add `include_self=False` parameter to functions which return lists/querysets

    Add `include_self=False` parameter to functions which return lists/querysets

    django_mptt had a nice option to include the current node in the functions which return descendants, children, etc. It is documented here

    It would be nice to be able to call get_children(include_self=True) or get_children_queryset(include_self=True), and get the node in the list as well

    enhancement 
    opened by jvacek 3
  • Queries getting slower during progress

    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: https://github.com/juliandehne/delab/issues/15

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

    Any ideas?

    enhancement 
    opened by juliandehne 11
  • Order control

    Order control

    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 :-(

    enhancement 
    opened by TimurKady 26
Releases(0.19.0)
Owner
Fabio Caccamo
Python/Django, MySQL, JavaScript/jQuery/Vue.js, Node/Gulp/Sass, Objective-C, ...
Fabio Caccamo
A starter template for building a backend with Django and django-rest-framework using docker with PostgreSQL as the primary DB.

Django-Rest-Template! This is a basic starter template for a backend project with Django as the server and PostgreSQL as the database. About the templ

Akshat Sharma 11 Dec 06, 2022
An insecure login and registration website with Django.

An insecure login and registration website with Django.

Luis Quiñones Requelme 1 Dec 05, 2021
Projeto onde podes inserir notícias, ver todas as notícias guardas e filtrar por tag. A base de dados usada é o mongoDB.

djangoProject Projeto onde podes inserir notícias, ver todas as notícias guardas e filtrar por tag. A base de dados usada é o mongoDB. packages utiliz

Sofia Rocha 1 Feb 22, 2022
Customize the behavior of django.contrib.auth permissions.

Customizando o comportamento do django.contrib.auth. O que queremos? Não criar as permissões padrões automaticamente (add, delete, view, read). Criar

Henrique Bastos 7 Nov 26, 2022
Built from scratch to replicate some of the Django admin functionality and add some more, to serve as an introspective interface for Django and Mongo.

django-mongonaut Info: An introspective interface for Django and MongoDB. Version: 0.2.21 Maintainer: Jazzband (jazzband.co) This Project is Being Mov

Jazzband 238 Dec 26, 2022
Django + NextJS + Tailwind Boilerplate

django + NextJS + Tailwind Boilerplate About A Django project boilerplate/templa

Shayan Debroy 3 Mar 11, 2022
Social Media Network Focuses On Data Security And Being Community Driven Web App

privalise Social Media Network Focuses On Data Security And Being Community Driven Web App The Main Idea: We`ve seen social media web apps that focuse

Privalise 8 Jun 25, 2021
A blog app powered by python-django

Django_BlogApp This is a blog app powered by python-django Features Add and delete blog post View someone else blog Can add comment to that blog And o

Manish Jalui 1 Sep 12, 2022
Utility for working with recurring dates in Django.

django-recurrence django-recurrence is a utility for working with recurring dates in Django. Documentation is available at https://django-recurrence.r

408 Jan 06, 2023
Getdp-project - A Django-built web app that generates a personalized banner of events to come

getdp-project https://get-my-dp.herokuapp.com/ A Django-built web app that gener

CODE 4 Aug 01, 2022
The Django Leaflet Admin List package provides an admin list view featured by the map and bounding box filter for the geo-based data of the GeoDjango.

The Django Leaflet Admin List package provides an admin list view featured by the map and bounding box filter for the geo-based data of the GeoDjango. It requires a django-leaflet package.

Vsevolod Novikov 33 Nov 11, 2022
Django-shared-app-isolated-databases-example - Django - Shared App & Isolated Databases

Django - Shared App & Isolated Databases An app that demonstrates the implementa

Ajai Danial 5 Jun 27, 2022
Django based webapp pulling in crypto news and price data via api

Deploy Django in Production FTA project implementing containerization of Django Web Framework into Docker to be placed into Azure Container Services a

0 Sep 21, 2022
Automatic class scheduler for Texas A&M written with Python+Django and React+Typescript

Rev Registration Description Rev Registration is an automatic class scheduler for Texas A&M, aimed at easing the process of course registration by gen

Aggie Coding Club 21 Nov 15, 2022
Django CRUD REST API Generator

Django CRUD REST API Generator This is a simple tool that generates a Django REST API with the given models. Specs: Authentication, DRF generic views,

Mehmet Alp Sümer 57 Nov 24, 2022
Template for Django Project Using Docker

You want a Django project who use Docker and Docker-compose for Development and for Production ? It's for you !

1 Dec 17, 2021
A Django Demo Project of Students Management System

Django_StudentMS A Django Demo Project of Students Management System. From NWPU Seddon for DB Class Pre. Seddon simplify the code in 2021/10/17. Hope

2 Dec 08, 2021
Inject an ID into every log message from a Django request. ASGI compatible, integrates with Sentry, and works with Celery

Django GUID Now with ASGI support! Django GUID attaches a unique correlation ID/request ID to all your log outputs for every request. In other words,

snok 300 Dec 29, 2022
Django React Project Setup

Django-React-Project-Setup INSTALLATION: python -m pip install drps USAGE: in your cmd: python -m drps Starting fullstack project with Django and Reac

Ghazi Zabalawi 7 Feb 06, 2022
Awesome Django Blog App

Awesome-Django-Blog-App Made with love django as the backend and Bootstrap as the frontend ! i hope that can help !! Project Title Django provides mul

ANAS NABIL 2 Feb 08, 2022