├── django_dyn_dt ├── __init__.py ├── models.py ├── admin.py ├── tests.py ├── apps.py ├── templates │ ├── static │ │ ├── data │ │ │ └── index.js │ │ └── src │ │ │ ├── events │ │ │ └── index.js │ │ │ ├── style │ │ │ └── myStyle.css │ │ │ ├── form │ │ │ └── index.js │ │ │ ├── index.js │ │ │ ├── images │ │ │ ├── csv.svg │ │ │ ├── excel.svg │ │ │ └── pdf.svg │ │ │ └── controller │ │ │ └── index.js │ ├── 404.html │ └── index.html ├── urls.py ├── helpers.py └── views.py ├── docs └── blank.txt ├── .gitignore ├── publish.txt ├── MANIFEST.in ├── LICENSE.md ├── setup.py ├── CHANGELOG.md └── README.md /django_dyn_dt/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/blank.txt: -------------------------------------------------------------------------------- 1 | "coming soon" -------------------------------------------------------------------------------- /django_dyn_dt/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /django_dyn_dt/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /django_dyn_dt/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.DS_Store 3 | *.egg* 4 | /dist/ 5 | /.idea 6 | /docs/_build/ 7 | /node_modules/ 8 | build/ 9 | -------------------------------------------------------------------------------- /publish.txt: -------------------------------------------------------------------------------- 1 | python setup.py sdist 2 | 3 | twine check dist/* 4 | 5 | twine upload .\dist\THE_GENERATED_PACKAGE 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.md 2 | include README.md 3 | recursive-include django_dyn_dt/templates * 4 | recursive-include docs * 5 | -------------------------------------------------------------------------------- /django_dyn_dt/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DynDatatablesConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'django_dyn_dt' 7 | -------------------------------------------------------------------------------- /django_dyn_dt/templates/static/data/index.js: -------------------------------------------------------------------------------- 1 | export let myData = {} 2 | export let modelName = '' 3 | 4 | export const setData = (headings , data,isDate) => { 5 | myData = { 6 | headings: headings, 7 | data: data, 8 | isDate: isDate, 9 | } 10 | } 11 | 12 | export const setModelName = (data) => { 13 | modelName = data 14 | } -------------------------------------------------------------------------------- /django_dyn_dt/templates/static/src/events/index.js: -------------------------------------------------------------------------------- 1 | 2 | export const events = (dataTable) => { 3 | // on change pages 4 | 5 | let searchQuery = new URLSearchParams(window.location.search).get('search') 6 | 7 | if (searchQuery === null) 8 | searchQuery = '' 9 | 10 | dataTable.on('datatable.perpage', function(perPage) { 11 | window.location.search = '?page=1&entries=' + perPage + 12 | "&search=" + searchQuery ; 13 | 14 | console.log(perPage) 15 | }); 16 | } -------------------------------------------------------------------------------- /django_dyn_dt/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | from .views import data_table_view, add_record, delete_record, edit_record, export 4 | 5 | urlpatterns = [ 6 | path('datatb//', data_table_view), 7 | path('datatb//add/', add_record), 8 | path('datatb//edit//', edit_record), 9 | path('datatb//delete//', delete_record), 10 | path('datatb//export/', export), 11 | ] 12 | -------------------------------------------------------------------------------- /django_dyn_dt/templates/static/src/style/myStyle.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: rgb(247, 247, 247); 3 | font-family: system-ui, -apple-system, "Segoe UI", Roboto; 4 | } 5 | 6 | tr:hover { 7 | background-color: rgb(230, 229, 229); 8 | } 9 | 10 | .item { 11 | margin-bottom: 40px; 12 | } 13 | 14 | div.datatable-top::after { 15 | display: none; !important; 16 | } 17 | 18 | #search-container { 19 | width: 10%; 20 | } 21 | 22 | img { 23 | height: 40px; 24 | } 25 | 26 | img:hover { 27 | cursor: pointer; 28 | } -------------------------------------------------------------------------------- /django_dyn_dt/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 not found 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

404

15 |

16 | page not found! 17 |

18 |
19 | 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 [App Generator](https://appseed.us) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import find_packages, setup 3 | 4 | with open(os.path.join(os.path.dirname(__file__), 'README.md')) as readme: 5 | README = readme.read() 6 | 7 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 8 | 9 | setup( 10 | name='django-dynamic-datatb', 11 | version='1.0.26', 12 | zip_safe=False, 13 | packages=find_packages(), 14 | include_package_data=True, 15 | description='Django Dynamic Datatables', 16 | long_description=README, 17 | long_description_content_type="text/markdown", 18 | url='https://app-generator.dev/docs/developer-tools/dynamic-datatables.html', 19 | author='AppSeed.us', 20 | author_email='support@appseed.us', 21 | license='MIT License', 22 | install_requires=[ 23 | 'djangorestframework', 24 | 'pandas', 25 | 'matplotlib', 26 | ], 27 | classifiers=[ 28 | 'Intended Audience :: Developers', 29 | 'Intended Audience :: System Administrators', 30 | 'License :: OSI Approved :: MIT License', 31 | 'Operating System :: OS Independent', 32 | 'Programming Language :: Python', 33 | 'Programming Language :: Python :: 2.6', 34 | 'Programming Language :: Python :: 2.7', 35 | 'Programming Language :: Python :: 3.2', 36 | 'Programming Language :: Python :: 3.3', 37 | 'Programming Language :: Python :: 3.4', 38 | 'Programming Language :: Python :: 3.5', 39 | 'Programming Language :: Python :: 3.6', 40 | 'Environment :: Web Environment', 41 | 'Topic :: Software Development', 42 | 'Topic :: Software Development :: User Interfaces', 43 | ], 44 | ) 45 | -------------------------------------------------------------------------------- /django_dyn_dt/templates/static/src/form/index.js: -------------------------------------------------------------------------------- 1 | 2 | import {myData} from '../../data/index.js' 3 | 4 | export const formTypes = { 5 | ADD: 'add', 6 | EDIT: 'edit' 7 | } 8 | 9 | export const formConstructor = (formType,item) => { 10 | 11 | // create form 12 | const form = document.getElementById('form') 13 | form.className = 'd-flex flex-column gap-1 p-3' 14 | form.innerHTML = '' 15 | 16 | myData.headings.forEach((d,i) => { 17 | 18 | const label = document.createElement('label') 19 | label.setAttribute('htmlFor',i) 20 | label.innerHTML = d; 21 | label.className = "form-label m-0"; 22 | 23 | const input = document.createElement('input'); 24 | 25 | if (myData.isDate[i] === 'True') 26 | input.setAttribute('type','date'); 27 | else 28 | input.setAttribute('type','text'); 29 | 30 | input.className = 'form-control m-0' 31 | input.placeholder = d 32 | 33 | if (d === 'id') 34 | input.setAttribute('disabled','true') 35 | 36 | if (formType === formTypes.EDIT) 37 | input.setAttribute('value', item[i]) 38 | 39 | form.innerHTML += label.outerHTML 40 | form.innerHTML += input.outerHTML 41 | }) 42 | 43 | if (formType === formTypes.ADD) { 44 | document.querySelector('.modal-title').innerHTML = 'add Item' 45 | document.querySelector('.modal-btn').value = 'add' 46 | 47 | } else if (formType === formTypes.EDIT) { 48 | document.querySelector('.modal-title').innerHTML = 'edit Item' 49 | document.querySelector('.modal-btn').value = 'edit' 50 | } 51 | 52 | return form; 53 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [1.0.26] 2024-10-17 4 | ### Changes 5 | 6 | - Update RM 7 | - [Django Dynamic DataTables](https://app-generator.dev/docs/developer-tools/dynamic-datatables.html) 8 | - Mention [Dynamic Django](https://app-generator.dev/docs/developer-tools/dynamic-django/index.html) Starter (commercial) 9 | 10 | ## [1.0.25] 2023-07-03 11 | ### Changes 12 | 13 | - Fix the Exports 14 | - PDF & CSV 15 | 16 | ## [1.0.24] 2023-06-28 17 | ### Changes 18 | 19 | - Bump Version 20 | - Remove `Widget Mode` (unstable release) 21 | - Fallback to single page mode 22 | 23 | ## [1.0.10] 2023-02-15 24 | ### Changes 25 | 26 | - DOCS Update (readme) 27 | 28 | ## [1.0.9] 2023-02-15 29 | ### Changes 30 | 31 | - DOCS Update (readme) 32 | 33 | ## [1.0.8] 2023-02-15 34 | ### Changes 35 | 36 | - DOCS Update (minor) 37 | 38 | ## [1.0.7] 2023-02-15 39 | ### Changes 40 | 41 | - Fixes & Stability 42 | - Minor Code refactoring 43 | 44 | ## [1.0.6] 2022-11-01 45 | ### Changes 46 | 47 | - Update DOCS 48 | 49 | ## [1.0.5] 2022-10-31 50 | ### Changes 51 | 52 | - Update DOCS 53 | 54 | ## [1.0.4] 2022-10-30 55 | ### Improvements 56 | 57 | - Fix DOCS (minor) 58 | 59 | ## [1.0.3] 2022-10-30 60 | ### Improvements 61 | 62 | - Fix DOCS (minor) 63 | 64 | ## [1.0.2] 2022-10-30 65 | ### Improvements 66 | 67 | - Better DOCS 68 | 69 | ## [1.0.1] 2022-10-30 70 | ### Improvements 71 | 72 | - Better DOCS 73 | 74 | ## [1.0.0] 2022-10-30 75 | ### STABLE_VERSION 76 | 77 | - Docs Update 78 | - Package is now complete 79 | 80 | ## [0.0.3] 2022-10-30 81 | ### Fixes 82 | 83 | - dummy 84 | 85 | ## [0.0.2] 2022-10-30 86 | ### Dummy 87 | 88 | - added code 89 | 90 | ## [0.0.1] 2022-10-30 91 | ### Initial Version 92 | 93 | - Codebase design 94 | -------------------------------------------------------------------------------- /django_dyn_dt/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | import datetime, sys, inspect, importlib 7 | 8 | from functools import wraps 9 | 10 | 11 | from django.db import models 12 | from rest_framework import serializers 13 | 14 | from functools import wraps 15 | 16 | from django.http import HttpResponseRedirect, HttpResponse 17 | 18 | class Utils: 19 | @staticmethod 20 | def get_class(config, name: str) -> models.Model: 21 | return Utils.model_name_to_class(config[name]) 22 | 23 | @staticmethod 24 | def get_manager(config, name: str) -> models.Manager: 25 | return Utils.get_class(config, name).objects 26 | 27 | @staticmethod 28 | def get_serializer(config, name: str): 29 | class Serializer(serializers.ModelSerializer): 30 | class Meta: 31 | model = Utils.get_class(config, name) 32 | fields = '__all__' 33 | 34 | return Serializer 35 | 36 | @staticmethod 37 | def model_name_to_class(name: str): 38 | 39 | model_name = name.split('.')[-1] 40 | model_import = name.replace('.'+model_name, '') 41 | 42 | module = importlib.import_module(model_import) 43 | cls = getattr(module, model_name) 44 | 45 | return cls 46 | 47 | def check_permission(function): 48 | @wraps(function) 49 | def wrap(viewRequest, *args, **kwargs): 50 | 51 | try: 52 | 53 | # Check user 54 | if viewRequest.request.user.is_authenticated: 55 | return function(viewRequest, *args, **kwargs) 56 | 57 | # All good - allow the processing 58 | return HttpResponseRedirect('/login/') 59 | 60 | except Exception as e: 61 | 62 | # On error 63 | return HttpResponse( 'Error: ' + str( e ) ) 64 | 65 | return function(viewRequest, *args, **kwargs) 66 | 67 | return wrap 68 | -------------------------------------------------------------------------------- /django_dyn_dt/templates/static/src/index.js: -------------------------------------------------------------------------------- 1 | import {modelName, myData} from '../data/index.js' 2 | import {events} from './events/index.js' 3 | import { 4 | removeRow, 5 | addRow, 6 | editRow, 7 | search, 8 | columnsManage, 9 | exportController, 10 | addController, 11 | middleContainer, 12 | } from './controller/index.js' 13 | import { formConstructor, formTypes } from './form/index.js' 14 | 15 | let formType = formTypes.ADD 16 | 17 | // table 18 | const dataTable = new simpleDatatables.DataTable('table' , { 19 | data: myData, 20 | perPageSelect: [10,25,50], 21 | perPage: parseInt(new URLSearchParams(window.location.search).get('entries')) || 10, 22 | labels: { 23 | placeholder: "Search...", 24 | perPage: "{select} Items/Page", 25 | noRows: "No entries to found", 26 | info: "Showing {start} to {end} of {rows} entries", 27 | }, 28 | searchable: false, 29 | }) 30 | 31 | // edit & remove Button 32 | const newColumn = [] 33 | myData.data.forEach((d,i) => { 34 | 35 | const editBtn = `` 36 | 37 | const removeBtn = `` 38 | 39 | newColumn.push(editBtn + "   " + removeBtn) 40 | }) 41 | // add buttons 42 | dataTable.columns().add({ 43 | heading: '', 44 | data: newColumn 45 | }) 46 | // add funcs 47 | dataTable.table.addEventListener('click', (e) => { 48 | if (e.target.nodeName === 'I' ) { 49 | const row = e.target.closest('tr'); 50 | if (e.target.className.includes('remove')) { 51 | removeRow(dataTable,row.dataIndex) 52 | } 53 | else if (e.target.className.includes('edit')) { 54 | const rowContent = [].slice.call(dataTable.data[row.dataIndex].cells).map((cell) => { return cell.textContent; }); 55 | formType = formTypes.EDIT 56 | formConstructor(formTypes.EDIT,rowContent) 57 | } 58 | } 59 | }) 60 | 61 | window.onload = () => { 62 | if (sessionStorage.getItem('register') == null) 63 | localStorage.removeItem('hideColumns'); 64 | 65 | sessionStorage.setItem('register', 1); 66 | 67 | const hideColumns = JSON.parse(localStorage.getItem('hideColumns')) 68 | hideColumns.forEach(d => { 69 | dataTable.columns().hide([myData.headings.indexOf(d)]) 70 | }) 71 | 72 | const els = document.getElementsByClassName('form-check-input') 73 | Array.prototype.forEach.call(els, function(el) { 74 | if (hideColumns.includes(el.id)) { 75 | el.checked = true 76 | } 77 | }); 78 | } 79 | 80 | // events 81 | events(dataTable) 82 | 83 | // To be coded 84 | //middleContainer(dataTable) 85 | 86 | // columns manage 87 | columnsManage(dataTable) 88 | 89 | // add 90 | addController(formType) 91 | 92 | // export 93 | exportController(dataTable) 94 | 95 | // Search Box 96 | search() 97 | 98 | document.addEventListener('submit',(e) => { 99 | 100 | e.preventDefault(); 101 | 102 | if (formType === formTypes.ADD) 103 | addRow(dataTable, getFormData()) 104 | else if (formType === formTypes.EDIT) 105 | editRow(dataTable, getFormData()) 106 | 107 | // console.log(getFormData()); 108 | }) 109 | 110 | const getFormData = () => { 111 | const data = {} 112 | const myForm = document.querySelector('form') 113 | 114 | for (let i of myForm.elements) { 115 | if (i.type === 'text' || i.type === 'date') 116 | data[i.placeholder] = i.value; 117 | } 118 | 119 | return data; 120 | } 121 | 122 | // style 123 | // modelName 124 | document.querySelector('.model-name').innerHTML = modelName 125 | document.querySelector('.dataTable-top').className += ' d-flex justify-content-between' 126 | -------------------------------------------------------------------------------- /django_dyn_dt/templates/static/src/images/csv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /django_dyn_dt/templates/static/src/images/excel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 27 | 33 | 36 | 41 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /django_dyn_dt/templates/static/src/images/pdf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 30 | 31 | 32 | 36 | 40 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Django Dynamic DataTables](https://app-generator.dev/docs/developer-tools/dynamic-datatables.html) 2 | 3 | `Open-Source` library for **Django** that provides a `powerful data table interface` (paginated information) with minimum effort - actively supported by **[App-Generator](https://app-generator.dev/)**. 4 | 5 | - [Django Dynamic DataTables](https://www.youtube.com/watch?v=EtMCK5AmdQI) - video presentation 6 | 7 |
8 | 9 | --- 10 | 11 | > For a **complete set of features** and long-term support, check out **[Dynamic Django](https://app-generator.dev/docs/developer-tools/dynamic-django/index.html)**, a powerful starter that incorporates: 12 | 13 | - [Dynamic DataTables](https://app-generator.dev/docs/developer-tools/dynamic-django/datatables.html): using a single line of configuration, the data saved in any table is automatically managed 14 | - [Dynamic API](https://app-generator.dev/docs/developer-tools/dynamic-django/api.html): any model can become a secure API Endpoint using DRF 15 | - [Dynamic Charts](https://app-generator.dev/docs/developer-tools/dynamic-django/charts.html): extract relevant charts without coding all major types are supported 16 | - [CSV Loader](https://app-generator.dev/docs/developer-tools/dynamic-django/csv-loader.html): translate CSV files into Django Models and (optional) load the information 17 | - Powerful [CLI Tools](https://app-generator.dev/docs/developer-tools/dynamic-django/cli.html) for the GIT interface, configuration editing, updating the configuration and database (create models, migrate DB) 18 | 19 |
20 | 21 | > Features 22 | 23 | - Modern Stack: `Django` & `VanillaJS` 24 | - `DT` layer provided by [Simple-DataTables](https://github.com/fiduswriter/Simple-DataTables) 25 | - `Server-side` pagination 26 | - Search, Filters 27 | - Exports in PDF, CSV formats 28 | - `MIT License` (commercial use allowed) 29 | 30 |
31 | 32 | ![Django Dynamic DataTables - Open-Source tool provided by AppSeed.](https://user-images.githubusercontent.com/51070104/194712823-b8bf1a9e-f5d8-47b3-b7e6-a46a29f3acbe.gif) 33 | 34 |
35 | 36 | ## How to use it 37 | 38 |
39 | 40 | > **Step #1** - `Install the package` 41 | 42 | ```bash 43 | $ pip install django-dynamic-datatb 44 | // OR 45 | $ pip install git+https://github.com/app-generator/django-dynamic-datatb.git 46 | ``` 47 | 48 |
49 | 50 | > **Step #2** - Update Configuration, `add new imports` 51 | 52 | ```python 53 | import os, inspect 54 | import django_dyn_dt 55 | ``` 56 | 57 |
58 | 59 | > **Step #3** - Update Configuration, `include the new APPs` 60 | 61 | ```python 62 | INSTALLED_APPS = [ 63 | 'django_dyn_dt', # <-- NEW App 64 | ] 65 | ``` 66 | 67 |
68 | 69 | > **Step #4** - Update Configuration, include the new `TEMPLATES` DIR 70 | 71 | ```python 72 | 73 | TEMPLATE_DIR_DATATB = os.path.join(BASE_DIR, "django_dyn_dt/templates") # <-- NEW App 74 | 75 | TEMPLATES = [ 76 | { 77 | "BACKEND": "django.template.backends.django.DjangoTemplates", 78 | "DIRS": [TEMPLATE_DIR_DATATB], # <-- NEW Include 79 | "APP_DIRS": True, 80 | "OPTIONS": { 81 | }, 82 | }, 83 | ] 84 | ``` 85 | 86 |
87 | 88 | > **Step #5** - Update Configuration, update `STATICFILES_DIRS` DIR 89 | 90 | ```python 91 | DYN_DB_PKG_ROOT = os.path.dirname( inspect.getfile( django_dyn_dt ) ) # <-- NEW App 92 | 93 | STATICFILES_DIRS = ( 94 | os.path.join(DYN_DB_PKG_ROOT, "templates/static"), 95 | ) 96 | ``` 97 | 98 |
99 | 100 | > **Step #6** - `Register the model` in `settings.py` (DYNAMIC_DATATB section) 101 | 102 | This sample code assumes that `app1` exists and model `Book` is defined and migrated. 103 | 104 | ```python 105 | 106 | DYNAMIC_DATATB = { 107 | # SLUG -> Import_PATH 108 | 'books' : "app1.models.Book", 109 | } 110 | 111 | ``` 112 | 113 |
114 | 115 | 116 | > **Step #7** - `Update routing`, include APIs 117 | 118 | ```python 119 | from django.contrib import admin 120 | from django.urls import path, include # <-- NEW: 'include` directive added 121 | 122 | urlpatterns = [ 123 | path("admin/", admin.site.urls), 124 | path('', include('django_dyn_dt.urls')), # <-- NEW: API routing rules 125 | ] 126 | ``` 127 | 128 |
129 | 130 | > **Step #8** - Use the Dynamic Datatable module 131 | 132 | If the managed model is `Books`, the dynamic interface is `/datatb/books/` and all features available. 133 | 134 |
135 | 136 | ![Django Dynamic DataTables - Open-Source Tool for Developers.](https://user-images.githubusercontent.com/51070104/194706034-b691226d-f9fa-4c05-a828-fc947670c573.jpg) 137 | 138 |
139 | 140 | ### Links & resources 141 | 142 | - [DRF](https://www.django-rest-framework.org/) - HOMEpage 143 | - More [Developer Tools](https://appseed.us/developer-tools/) provided by `AppSeed` 144 | - Ask for [Support](https://appseed.us/support/) via `Email` & `Discord` 145 | 146 |
147 | 148 | --- 149 | [Django Dynamic DataTables](https://app-generator.dev/docs/developer-tools/dynamic-datatables.html) - Open-source library provided by **[App-Generator](https://app-generator.dev/)** 150 | -------------------------------------------------------------------------------- /django_dyn_dt/templates/index.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Dynamic Data Table 22 | 23 | 24 | 25 |
26 | 27 | 28 |
29 |

Model Name

30 |
31 | 32 | 33 |
34 |
35 |
36 | 37 |
38 | 39 | 40 | 83 | 84 | 85 | 111 | 112 | 113 |
114 | 122 |
123 | 124 | 125 | 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /django_dyn_dt/views.py: -------------------------------------------------------------------------------- 1 | 2 | import os, json, math, random, string, base64 3 | 4 | from django.core.exceptions import ValidationError 5 | from django.db.models import Q 6 | from django.db.models.fields.related import RelatedField 7 | from django.http import HttpResponse 8 | from django.shortcuts import render 9 | 10 | # Create your views here. 11 | from django.views.decorators.csrf import csrf_exempt 12 | 13 | from .helpers import Utils 14 | 15 | from django.db.models.fields import DateField 16 | 17 | import pandas as pd 18 | import matplotlib.pyplot as plt 19 | from matplotlib.backends.backend_pdf import PdfPages 20 | 21 | from django.conf import settings 22 | 23 | DYNAMIC_DATATB = {} 24 | 25 | try: 26 | DYNAMIC_DATATB = getattr(settings, 'DYNAMIC_DATATB') 27 | except: 28 | pass 29 | 30 | # TODO: 404 for wrong page number 31 | def data_table_view(request, **kwargs): 32 | try: 33 | model_class = Utils.get_class(DYNAMIC_DATATB, kwargs.get('model_name')) 34 | except KeyError: 35 | return render(request, '404.html', status=404) 36 | headings = _get_headings(model_class) 37 | page_number = int(request.GET.get('page', 1)) 38 | search_key = request.GET.get('search', '') 39 | entries = int(request.GET.get('entries', 10)) 40 | 41 | if page_number < 1: 42 | return render(request, '404.html', status=404) 43 | 44 | filter_options = Q() 45 | for field in headings: 46 | filter_options = filter_options | Q(**{field + '__icontains': search_key}) 47 | all_data = model_class.objects.filter(filter_options) 48 | data = all_data[(page_number - 1) * entries:page_number * entries] 49 | if all_data.count() != 0 and not 1 <= page_number <= math.ceil(all_data.count() / entries): 50 | return render(request, '404.html', status=404) 51 | return render(request, 'index.html', context={ 52 | 'model_name': kwargs.get('model_name'), 53 | 'headings': headings, 54 | 'data': [[getattr(record, heading) for heading in headings] for record in data], 55 | 'is_date': [True if type(field) == DateField else False for field in model_class._meta.get_fields()], 56 | 'total_pages': range(1, math.ceil(all_data.count() / entries) + 1), 57 | 'has_prev': False if page_number == 1 else ( 58 | True if all_data.count() != 0 else False), 59 | 'has_next': False if page_number == math.ceil(all_data.count() / entries) else ( 60 | True if all_data.count() != 0 else False), 61 | 'current_page': page_number, 62 | 'entries': entries, 63 | 'search': search_key, 64 | }) 65 | 66 | 67 | @csrf_exempt 68 | def add_record(request, **kwargs): 69 | try: 70 | model_manager = Utils.get_manager(DYNAMIC_DATATB, kwargs.get('model_name')) 71 | except KeyError: 72 | return HttpResponse(json.dumps({ 73 | 'message': 'this model is not activated or not exist.', 74 | 'success': False 75 | }), status=400) 76 | body = json.loads(request.body.decode("utf-8")) 77 | try: 78 | thing = model_manager.create(**body) 79 | except Exception as ve: 80 | return HttpResponse(json.dumps({ 81 | 'detail': str(ve), 82 | 'success': False 83 | }), status=400) 84 | return HttpResponse(json.dumps({ 85 | 'id': thing.id, 86 | 'message': 'Record Created.', 87 | 'success': True 88 | }), status=200) 89 | 90 | 91 | @csrf_exempt 92 | def delete_record(request, **kwargs): 93 | try: 94 | model_manager = Utils.get_manager(DYNAMIC_DATATB, kwargs.get('model_name')) 95 | except KeyError: 96 | return HttpResponse(json.dumps({ 97 | 'message': 'this model is not activated or not exist.', 98 | 'success': False 99 | }), status=400) 100 | to_delete_id = kwargs.get('id') 101 | try: 102 | to_delete_object = model_manager.get(id=to_delete_id) 103 | except Exception: 104 | return HttpResponse(json.dumps({ 105 | 'message': 'matching object not found.', 106 | 'success': False 107 | }), status=404) 108 | to_delete_object.delete() 109 | return HttpResponse(json.dumps({ 110 | 'message': 'Record Deleted.', 111 | 'success': True 112 | }), status=200) 113 | 114 | 115 | @csrf_exempt 116 | def edit_record(request, **kwargs): 117 | try: 118 | model_manager = Utils.get_manager(DYNAMIC_DATATB, kwargs.get('model_name')) 119 | except KeyError: 120 | return HttpResponse(json.dumps({ 121 | 'message': 'this model is not activated or not exist.', 122 | 'success': False 123 | }), status=400) 124 | to_update_id = kwargs.get('id') 125 | 126 | body = json.loads(request.body.decode("utf-8")) 127 | try: 128 | model_manager.filter(id=to_update_id).update(**body) 129 | except Exception as ve: 130 | return HttpResponse(json.dumps({ 131 | 'detail': str(ve), 132 | 'success': False 133 | }), status=400) 134 | return HttpResponse(json.dumps({ 135 | 'message': 'Record Updated.', 136 | 'success': True 137 | }), status=200) 138 | 139 | 140 | @csrf_exempt 141 | def export(request, **kwargs): 142 | try: 143 | model_class = Utils.get_class(DYNAMIC_DATATB, kwargs.get('model_name')) 144 | except KeyError: 145 | return render(request, '404.html', status=404) 146 | request_body = json.loads(request.body.decode('utf-8')) 147 | search_key = request_body.get('search', '') 148 | hidden = request_body.get('hidden_cols', []) 149 | export_type = request_body.get('type', 'csv') 150 | filter_options = Q() 151 | 152 | headings = list(_get_headings(model_class)) 153 | for field in headings: 154 | field_name = field 155 | try: 156 | filter_options = filter_options | Q(**{field_name + '__icontains': search_key}) 157 | except Exception as _: 158 | pass 159 | 160 | all_data = model_class.objects.filter(filter_options) 161 | table_data = [] 162 | for data in all_data: 163 | this_row = [] 164 | for heading in headings: 165 | this_row.append(getattr(data, heading)) 166 | table_data.append(this_row) 167 | 168 | df = pd.DataFrame( 169 | table_data, 170 | columns=tuple(heading for heading in headings)) 171 | if export_type == 'pdf': 172 | base64encoded = get_pdf(df) 173 | elif export_type == 'xlsx': 174 | base64encoded = get_excel(df) 175 | elif export_type == 'csv': 176 | base64encoded = get_csv(df) 177 | else: 178 | base64encoded = 'nothing' 179 | 180 | return HttpResponse(json.dumps({ 181 | 'content': base64encoded, 182 | 'file_format': export_type, 183 | 'success': True 184 | }), status=200) 185 | 186 | 187 | def get_pdf(data_frame, ): 188 | fig, ax = plt.subplots(figsize=(12, 4)) 189 | ax.axis('tight') 190 | ax.axis('off') 191 | ax.table(cellText=data_frame.values, colLabels=data_frame.columns, loc='center', 192 | colLoc='center', ) 193 | random_file_name = get_random_string(10) + '.pdf' 194 | pp = PdfPages(random_file_name) 195 | pp.savefig(fig, bbox_inches='tight') 196 | pp.close() 197 | bytess = read_file_and_remove(random_file_name) 198 | return base64.b64encode(bytess).decode('utf-8') 199 | 200 | 201 | def get_excel(data_frame, ): 202 | random_file_name = get_random_string(10) + '.xlsx' 203 | 204 | data_frame.to_excel(random_file_name, index=False, header=True, encoding='utf-8') 205 | bytess = read_file_and_remove(random_file_name) 206 | return base64.b64encode(bytess).decode('utf-8') 207 | 208 | 209 | def get_csv(data_frame, ): 210 | random_file_name = get_random_string(10) + '.csv' 211 | 212 | data_frame.to_csv(random_file_name, index=False, header=True, encoding='utf-8') 213 | bytess = read_file_and_remove(random_file_name) 214 | return base64.b64encode(bytess).decode('utf-8') 215 | 216 | 217 | def read_file_and_remove(path): 218 | with open(path, 'rb') as file: 219 | bytess = file.read() 220 | file.close() 221 | 222 | # ths file pointer should be closed before removal 223 | os.remove(path) 224 | return bytess 225 | 226 | 227 | def get_random_string(length): 228 | # choose from all lowercase letter 229 | letters = string.ascii_lowercase 230 | return ''.join(random.choice(letters) for i in range(length)) 231 | 232 | 233 | def _get_headings(model_class, filter_relations=True): 234 | headings = [] 235 | for field in model_class._meta.get_fields(): 236 | if filter_relations and _is_relation_field(field): 237 | continue 238 | headings.append(field.name) 239 | return headings 240 | 241 | def _is_relation_field(field): 242 | is_many_to_many_field = field.many_to_many is not None 243 | is_many_to_one_field = field.many_to_one is not None 244 | is_one_to_many_field = field.one_to_many is not None 245 | is_one_to_one_field = field.one_to_one is not None 246 | return is_many_to_many_field or is_many_to_one_field or is_one_to_many_field or is_one_to_one_field -------------------------------------------------------------------------------- /django_dyn_dt/templates/static/src/controller/index.js: -------------------------------------------------------------------------------- 1 | import {modelName, myData} from "../../data/index.js"; 2 | import {formConstructor, formTypes} from "../form/index.js"; 3 | 4 | const editBtn = `` 5 | const removeBtn = `` 6 | 7 | const toastLive = document.getElementById('liveToast') 8 | const toast = new bootstrap.Toast(toastLive) 9 | 10 | const setToastBody = (text,type) => { 11 | document.querySelector('.toast-body').innerHTML = text 12 | 13 | toastLive.className = type === 'success' 14 | ? 15 | toastLive.className.replace(/bg-.+/,'bg-success') 16 | : 17 | toastLive.className.replace(/bg-.+/,'bg-danger') 18 | 19 | } 20 | 21 | // Add Button + Events 22 | export const addController = (formType) => { 23 | 24 | const myModalEl = document.getElementById('exampleModal'); 25 | const modal = new bootstrap.Modal(myModalEl , {}) 26 | 27 | const addContainer = document.createElement('div') 28 | 29 | const addBtn = document.createElement('button') 30 | addBtn.className = 'btn btn-primary mx-1' 31 | addBtn.textContent = '+' 32 | addBtn.id = 'add' 33 | 34 | addContainer.appendChild(addBtn) 35 | 36 | document.querySelector('.dropdown').insertBefore(addContainer , 37 | document.querySelector('#dropdownMenuButton1') 38 | ) 39 | 40 | addBtn.addEventListener('click' , (e) => { 41 | formType = formTypes.ADD 42 | formConstructor(formTypes.ADD) 43 | modal.show() 44 | }) 45 | } 46 | 47 | function search_action() { 48 | 49 | const searchValue = document.querySelector('#search').value 50 | 51 | const searchParams = new URLSearchParams(window.location.search) 52 | searchParams.set("search", searchValue); 53 | searchParams.set("page", '1'); 54 | const newRelativePathQuery = window.location.pathname + '?' + searchParams.toString(); 55 | window.history.pushState({},'',newRelativePathQuery) 56 | location.reload() 57 | } 58 | 59 | // Search Box + Events 60 | export const search = () => { 61 | 62 | const searchContainer = document.createElement('div') 63 | searchContainer.className = 'd-flex' 64 | searchContainer.id = 'search-container' 65 | 66 | const searchInput = document.createElement('input') 67 | searchInput.className = 'form-control mx-1' 68 | searchInput.setAttribute('placeholder', 'search...') 69 | searchInput.setAttribute('id','search') 70 | searchInput.setAttribute('type','text') 71 | 72 | const searchBtn = document.createElement('button') 73 | searchBtn.className = 'btn btn-primary' 74 | searchBtn.setAttribute('id','search-btn') 75 | searchBtn.innerHTML = '' 76 | 77 | searchContainer.appendChild(searchInput) 78 | searchContainer.appendChild(searchBtn) 79 | 80 | document.querySelector('.dataTable-top').appendChild(searchContainer) 81 | 82 | // Trigger Search on ENTER 83 | document.querySelector('#search').addEventListener("keypress", function(event) { 84 | if (event.key === "Enter") { 85 | search_action(); 86 | } 87 | }) 88 | 89 | // Trigger Search on Button Click 90 | document.querySelector('#search-btn').addEventListener('click',() => { 91 | search_action(); 92 | }) 93 | } 94 | 95 | // Unused 96 | export const middleContainer = (dataTable) => { 97 | 98 | const middleContainer = document.createElement('div') 99 | middleContainer.className = 'd-flex' 100 | middleContainer.id = 'middle-container' 101 | 102 | const span = document.createElement('span') 103 | span.className = 'mx-1' 104 | span.id = 'span1' 105 | span.textContent = 'Dummy' 106 | 107 | middleContainer.appendChild(span) 108 | 109 | document.querySelector('.dataTable-top').insertBefore(middleContainer, document.querySelector('#search-container')); 110 | } 111 | 112 | // Filter Combo (layout + Events) 113 | export const columnsManage = (dataTable) => { 114 | 115 | const dropDown = document.createElement('div') 116 | dropDown.className = 'dropdown d-flex' 117 | dropDown.id = 'filter-container' 118 | 119 | const button = document.createElement('button') 120 | button.className = 'btn dropdown-toggle' 121 | button.id = 'dropdownMenuButton1' 122 | button.setAttribute( 'data-bs-toggle' , 'dropdown') 123 | button.textContent = 'Filter' 124 | 125 | dropDown.appendChild(button) 126 | 127 | const ul = document.createElement( 'ul') 128 | ul.className = 'dropdown-menu' 129 | 130 | myData.headings.forEach((d,i) => { 131 | 132 | const li = document.createElement('li') 133 | li.className = 'dropdown-item' 134 | 135 | const check = document.createElement('input') 136 | check.className = 'form-check-input' 137 | check.id = d 138 | check.setAttribute('type','checkbox') 139 | 140 | const label = document.createElement('label') 141 | label.className = 'form-check-label mx-1' 142 | label.textContent = d 143 | 144 | li.appendChild(check) 145 | li.appendChild(label) 146 | 147 | ul.appendChild(li) 148 | }) 149 | 150 | dropDown.appendChild(ul) 151 | document.querySelector('.dataTable-top').insertBefore(dropDown, document.querySelector('#search-container')); 152 | 153 | dropDown.addEventListener('change' , (e) => { 154 | if (e.target.nodeName === 'INPUT') { 155 | 156 | const id = myData.headings.indexOf(e.target.closest('input').id) 157 | if (e.target.closest('input').checked) { 158 | dataTable.columns().hide([parseInt(id)]) 159 | const hideColumns = JSON.parse(localStorage.getItem('hideColumns')) || [] 160 | localStorage.setItem('hideColumns' , JSON.stringify([...hideColumns , e.target.closest('input').id])) 161 | } else { 162 | dataTable.columns().show([parseInt(id)]) 163 | const hideColumns = JSON.parse(localStorage.getItem('hideColumns')) || [] 164 | localStorage.setItem('hideColumns' , JSON.stringify(hideColumns.filter(d => d !== e.target.closest('input').id))) 165 | } 166 | 167 | } 168 | }) 169 | } 170 | 171 | // Export layout 172 | export const exportController = (dataTable) => { 173 | 174 | const exportContainer = document.createElement('div') 175 | exportContainer.id = 'export-container' 176 | exportContainer.className = 'mx-1' 177 | 178 | const pdfImg = document.createElement('img') 179 | pdfImg.setAttribute('src',"/static/src/images/pdf.svg") 180 | pdfImg.id = 'pdf' 181 | 182 | const csvImg = document.createElement('img') 183 | csvImg.setAttribute('src',"/static/src/images/csv.svg") 184 | csvImg.id = 'csv' 185 | 186 | /* 187 | const excelImg = document.createElement('img') 188 | excelImg.setAttribute('src',"/static/src/images/excel.svg") 189 | excelImg.id = 'excel' 190 | */ 191 | 192 | exportContainer.addEventListener('click' , (e) => { 193 | if (e.target.nodeName === 'IMG' ) { 194 | exportData(dataTable , e.target.id ) 195 | } 196 | }) 197 | 198 | exportContainer.appendChild(pdfImg) 199 | exportContainer.appendChild(csvImg) 200 | 201 | //exportContainer.appendChild(excelImg) 202 | 203 | document.querySelector('.dropdown').insertBefore(exportContainer , 204 | document.querySelector('#dropdownMenuButton1') 205 | ) 206 | 207 | //document.querySelector('.dropdown').appendChild(exportContainer); 208 | } 209 | 210 | // Action: Export 211 | export const exportData = (dataTable, type) => { 212 | 213 | const searchParam = new URLSearchParams(window.location.search).get('search') || '' 214 | 215 | const hiddenColumns = myData.headings.filter((d,i) => !dataTable.columns().visible(i)) 216 | 217 | fetch (`/datatb/${modelName}/export/`, 218 | {method: 'POST',body: JSON.stringify({ 219 | search: searchParam, 220 | hidden_cols: hiddenColumns, 221 | type: type === 'excel' ? 'xlsx' : type 222 | })}) 223 | .then((response) => { 224 | if(!response.ok) { 225 | return response.text().then(text => { throw new Error(text) }) 226 | } else { 227 | return response.json() 228 | } 229 | }) 230 | .then((result) => { 231 | let a = document.createElement("a"); 232 | a.href = `data:application/${result.file_format};base64,${result.content}` 233 | a.download = `data-table.${result.file_format === 'excel' ? 'xlsx' : result.file_format}` 234 | a.click(); 235 | }) 236 | .catch((err) => { 237 | console.log(err.toString()) 238 | }) 239 | } 240 | 241 | export const addRow = async (dataTable, item) => { 242 | 243 | const myModalEl = document.getElementById('exampleModal'); 244 | const modal = bootstrap.Modal.getInstance(myModalEl) 245 | 246 | delete item.id 247 | 248 | // server 249 | fetch (`/datatb/${modelName}/add/`, { 250 | method: "POST", 251 | body: JSON.stringify(item), 252 | }) 253 | .then((response) => { 254 | if(!response.ok) { 255 | return response.text().then(text => { throw new Error(text) }) 256 | } else { 257 | return response.json() 258 | } 259 | }) 260 | .then((result) => { 261 | dataTable.rows().add( 262 | [...Object.values({id: result.id.toString(),...item}),editBtn + " " + removeBtn] 263 | ) 264 | 265 | const alert = document.querySelector('.alert') 266 | alert.className = alert.className.replace('d-block','d-none') 267 | location.reload(); 268 | 269 | modal.hide(); 270 | }) 271 | .catch((err) => { 272 | const alert = document.querySelector('.alert') 273 | alert.textContent = JSON.parse(err.toString().replace('Error: ','')).detail 274 | alert.className = alert.className.replace('d-none','d-block') 275 | }) 276 | } 277 | 278 | export const editRow = (dataTable , item) => { 279 | 280 | const id = item.id 281 | delete item.id 282 | 283 | // server 284 | fetch (`/datatb/${modelName}/edit/${id}/`, { 285 | method: "POST", 286 | body: JSON.stringify(item), 287 | }) 288 | .then((response) => { 289 | if(!response.ok) { 290 | return response.text().then(text => { throw new Error(text) }) 291 | } else { 292 | return response.json() 293 | } 294 | }) 295 | .then((result) => { 296 | 297 | dataTable.data.forEach((d,i) => { 298 | if ( dataTable.data[i].cells[0].data === item.id ) { 299 | dataTable.rows().remove(i) 300 | } 301 | }) 302 | dataTable.rows().add( 303 | [...Object.values(item),editBtn + " " + removeBtn] 304 | ) 305 | 306 | const alert = document.querySelector('.alert') 307 | alert.className = alert.className.replace('d-block','d-none') 308 | location.reload(); 309 | }) 310 | .catch((err) => { 311 | const alert = document.querySelector('.alert') 312 | alert.textContent = JSON.parse(err.toString().replace('Error: ','')).detail 313 | alert.className = alert.className.replace('d-none','d-block') 314 | }) 315 | } 316 | 317 | export const removeRow = (dataTable , item) => { 318 | 319 | const id = dataTable.data[item].cells[0].data 320 | 321 | // server 322 | fetch (`/datatb/${modelName}/delete/${id}/`, { 323 | method: "POST", 324 | }) 325 | .then((response) => { 326 | if(!response.ok) { 327 | return response.text().then(text => { throw new Error(text) }) 328 | } else { 329 | return response.json() 330 | } 331 | }) 332 | .then((result) => { 333 | dataTable.rows().remove(item) 334 | 335 | setToastBody(result.message,'success') 336 | toast.show() 337 | location.reload() 338 | }) 339 | .catch((err) => { 340 | setToastBody(JSON.parse(err.toString().replace('Error: ','')).detail,'fail') 341 | toast.show() 342 | }) 343 | 344 | } 345 | --------------------------------------------------------------------------------