├── .gitignore ├── README.md ├── backend ├── __init__.py ├── asgi.py ├── core │ ├── __init__.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── static │ │ ├── css │ │ │ ├── icons │ │ │ │ └── simple-line-icons.min.css │ │ │ ├── style.css │ │ │ └── table.css │ │ └── img │ │ │ └── django-logo-negative.png │ ├── templates │ │ ├── base.html │ │ ├── includes │ │ │ ├── nav.html │ │ │ └── pagination.html │ │ └── index.html │ ├── templatetags │ │ ├── __init__.py │ │ ├── rows_tags.py │ │ └── url_replace.py │ ├── urls.py │ └── views.py ├── expense │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── templates │ │ ├── expense │ │ │ ├── expense_content.html │ │ │ ├── expense_list.html │ │ │ └── expense_table.html │ │ └── includes │ │ │ ├── ordering_field.html │ │ │ └── ordering_icon.html │ ├── urls.py │ └── views.py ├── settings.py ├── urls.py └── wsgi.py ├── contrib └── env_gen.py ├── manage.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .DS_Store 132 | 133 | media/ 134 | staticfiles/ 135 | .idea 136 | .ipynb_checkpoints/ 137 | .vscode 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django datatables htmx 2 | 3 | Layout based on https://www.cssscript.com/minimal-data-table/ 4 | 5 | ## This project was done with: 6 | 7 | * [Python 3.9.6](https://www.python.org/) 8 | * [Django 4.0](https://www.djangoproject.com/) 9 | 10 | ## How to run project? 11 | 12 | * Clone this repository. 13 | * Create virtualenv with Python 3. 14 | * Active the virtualenv. 15 | * Install dependences. 16 | * Run the migrations. 17 | 18 | ``` 19 | git clone https://github.com/rg3915/django-datatables-htmx.git 20 | cd django-datatables-htmx 21 | python -m venv .venv 22 | source .venv/bin/activate 23 | pip install -r requirements.txt 24 | python contrib/env_gen.py 25 | python manage.py migrate 26 | ``` 27 | 28 | ## Django Seed 29 | 30 | if running [django-seed](https://github.com/Brobin/django-seed) type: 31 | 32 | ``` 33 | python manage.py seed expense --number=145 34 | ``` 35 | 36 | 37 | # Django datatables htmx 38 | 39 | Layout baseado em https://www.cssscript.com/minimal-data-table/ 40 | 41 | ## Este projeto foi feito com: 42 | 43 | * [Python 3.9.6](https://www.python.org/) 44 | * [Django 4.0](https://www.djangoproject.com/) 45 | 46 | ## Como rodar o projeto? 47 | 48 | * Clone esse repositório. 49 | * Crie um virtualenv com Python 3. 50 | * Ative o virtualenv. 51 | * Instale as dependências. 52 | * Rode as migrações. 53 | 54 | ``` 55 | git clone https://github.com/rg3915/django-datatables-htmx.git 56 | cd django-datatables-htmx 57 | python -m venv .venv 58 | source .venv/bin/activate 59 | pip install -r requirements.txt 60 | python contrib/env_gen.py 61 | python manage.py migrate 62 | ``` 63 | 64 | 65 | ## Django Seed 66 | 67 | Se quiser rodar o [django-seed](https://github.com/Brobin/django-seed) digite: 68 | 69 | ``` 70 | python manage.py seed expense --number=145 71 | ``` 72 | 73 | -------------------------------------------------------------------------------- /backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-datatables-htmx/1abb4e0d2437cde0387a8409d6130de3d9818dc8/backend/__init__.py -------------------------------------------------------------------------------- /backend/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for backend project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /backend/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-datatables-htmx/1abb4e0d2437cde0387a8409d6130de3d9818dc8/backend/core/__init__.py -------------------------------------------------------------------------------- /backend/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'backend.core' 7 | -------------------------------------------------------------------------------- /backend/core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-datatables-htmx/1abb4e0d2437cde0387a8409d6130de3d9818dc8/backend/core/migrations/__init__.py -------------------------------------------------------------------------------- /backend/core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class TimeStampedModel(models.Model): 5 | created = models.DateTimeField( 6 | 'criado em', 7 | auto_now_add=True, 8 | auto_now=False 9 | ) 10 | modified = models.DateTimeField( 11 | 'modificado em', 12 | auto_now_add=False, 13 | auto_now=True 14 | ) 15 | 16 | class Meta: 17 | abstract = True 18 | -------------------------------------------------------------------------------- /backend/core/static/css/icons/simple-line-icons.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:simple-line-icons;src:url(../../fonts/Simple-Line-Icons.eot);src:url(../../fonts/Simple-Line-Icons.eot) format('embedded-opentype'),url(../../fonts/Simple-Line-Icons.woff2) format('woff2'),url(../../fonts/Simple-Line-Icons.ttf) format('truetype'),url(../../fonts/Simple-Line-Icons.woff) format('woff'),url(../../fonts/Simple-Line-Icons.svg#simple-line-icons) format('svg');font-weight:400;font-style:normal}.icon-action-redo,.icon-action-undo,.icon-anchor,.icon-arrow-down,.icon-arrow-down-circle,.icon-arrow-left,.icon-arrow-left-circle,.icon-arrow-right,.icon-arrow-right-circle,.icon-arrow-up,.icon-arrow-up-circle,.icon-badge,.icon-bag,.icon-ban,.icon-basket,.icon-basket-loaded,.icon-bell,.icon-book-open,.icon-briefcase,.icon-bubble,.icon-bubbles,.icon-bulb,.icon-calculator,.icon-calendar,.icon-call-end,.icon-call-in,.icon-call-out,.icon-camera,.icon-camrecorder,.icon-chart,.icon-check,.icon-chemistry,.icon-clock,.icon-close,.icon-cloud-download,.icon-cloud-upload,.icon-compass,.icon-control-end,.icon-control-forward,.icon-control-pause,.icon-control-play,.icon-control-rewind,.icon-control-start,.icon-credit-card,.icon-crop,.icon-cup,.icon-cursor,.icon-cursor-move,.icon-diamond,.icon-direction,.icon-directions,.icon-disc,.icon-dislike,.icon-doc,.icon-docs,.icon-drawer,.icon-drop,.icon-earphones,.icon-earphones-alt,.icon-emotsmile,.icon-energy,.icon-envelope,.icon-envelope-letter,.icon-envelope-open,.icon-equalizer,.icon-event,.icon-exclamation,.icon-eye,.icon-eyeglass,.icon-feed,.icon-film,.icon-fire,.icon-flag,.icon-folder,.icon-folder-alt,.icon-frame,.icon-game-controller,.icon-ghost,.icon-globe,.icon-globe-alt,.icon-graduation,.icon-graph,.icon-grid,.icon-handbag,.icon-heart,.icon-home,.icon-hourglass,.icon-info,.icon-key,.icon-layers,.icon-like,.icon-link,.icon-list,.icon-location-pin,.icon-lock,.icon-lock-open,.icon-login,.icon-logout,.icon-loop,.icon-magic-wand,.icon-magnet,.icon-magnifier,.icon-magnifier-add,.icon-magnifier-remove,.icon-map,.icon-menu,.icon-microphone,.icon-minus,.icon-mouse,.icon-music-tone,.icon-music-tone-alt,.icon-mustache,.icon-note,.icon-notebook,.icon-options,.icon-options-vertical,.icon-organization,.icon-paper-clip,.icon-paper-plane,.icon-paypal,.icon-pencil,.icon-people,.icon-phone,.icon-picture,.icon-pie-chart,.icon-pin,.icon-plane,.icon-playlist,.icon-plus,.icon-power,.icon-present,.icon-printer,.icon-puzzle,.icon-question,.icon-refresh,.icon-reload,.icon-rocket,.icon-screen-desktop,.icon-screen-smartphone,.icon-screen-tablet,.icon-settings,.icon-share,.icon-share-alt,.icon-shield,.icon-shuffle,.icon-size-actual,.icon-size-fullscreen,.icon-social-behance,.icon-social-dribbble,.icon-social-dropbox,.icon-social-facebook,.icon-social-foursqare,.icon-social-github,.icon-social-google,.icon-social-instagram,.icon-social-linkedin,.icon-social-pinterest,.icon-social-reddit,.icon-social-skype,.icon-social-soundcloud,.icon-social-spotify,.icon-social-steam,.icon-social-stumbleupon,.icon-social-tumblr,.icon-social-twitter,.icon-social-vkontakte,.icon-social-youtube,.icon-speech,.icon-speedometer,.icon-star,.icon-support,.icon-symbol-female,.icon-symbol-male,.icon-tag,.icon-target,.icon-trash,.icon-trophy,.icon-umbrella,.icon-user,.icon-user-female,.icon-user-follow,.icon-user-following,.icon-user-unfollow,.icon-vector,.icon-volume-1,.icon-volume-2,.icon-volume-off,.icon-wallet,.icon-wrench{font-family:simple-line-icons;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-user:before{content:"\e005"}.icon-people:before{content:"\e001"}.icon-user-female:before{content:"\e000"}.icon-user-follow:before{content:"\e002"}.icon-user-following:before{content:"\e003"}.icon-user-unfollow:before{content:"\e004"}.icon-login:before{content:"\e066"}.icon-logout:before{content:"\e065"}.icon-emotsmile:before{content:"\e021"}.icon-phone:before{content:"\e600"}.icon-call-end:before{content:"\e048"}.icon-call-in:before{content:"\e047"}.icon-call-out:before{content:"\e046"}.icon-map:before{content:"\e033"}.icon-location-pin:before{content:"\e096"}.icon-direction:before{content:"\e042"}.icon-directions:before{content:"\e041"}.icon-compass:before{content:"\e045"}.icon-layers:before{content:"\e034"}.icon-menu:before{content:"\e601"}.icon-list:before{content:"\e067"}.icon-options-vertical:before{content:"\e602"}.icon-options:before{content:"\e603"}.icon-arrow-down:before{content:"\e604"}.icon-arrow-left:before{content:"\e605"}.icon-arrow-right:before{content:"\e606"}.icon-arrow-up:before{content:"\e607"}.icon-arrow-up-circle:before{content:"\e078"}.icon-arrow-left-circle:before{content:"\e07a"}.icon-arrow-right-circle:before{content:"\e079"}.icon-arrow-down-circle:before{content:"\e07b"}.icon-check:before{content:"\e080"}.icon-clock:before{content:"\e081"}.icon-plus:before{content:"\e095"}.icon-minus:before{content:"\e615"}.icon-close:before{content:"\e082"}.icon-event:before{content:"\e619"}.icon-exclamation:before{content:"\e617"}.icon-organization:before{content:"\e616"}.icon-trophy:before{content:"\e006"}.icon-screen-smartphone:before{content:"\e010"}.icon-screen-desktop:before{content:"\e011"}.icon-plane:before{content:"\e012"}.icon-notebook:before{content:"\e013"}.icon-mustache:before{content:"\e014"}.icon-mouse:before{content:"\e015"}.icon-magnet:before{content:"\e016"}.icon-energy:before{content:"\e020"}.icon-disc:before{content:"\e022"}.icon-cursor:before{content:"\e06e"}.icon-cursor-move:before{content:"\e023"}.icon-crop:before{content:"\e024"}.icon-chemistry:before{content:"\e026"}.icon-speedometer:before{content:"\e007"}.icon-shield:before{content:"\e00e"}.icon-screen-tablet:before{content:"\e00f"}.icon-magic-wand:before{content:"\e017"}.icon-hourglass:before{content:"\e018"}.icon-graduation:before{content:"\e019"}.icon-ghost:before{content:"\e01a"}.icon-game-controller:before{content:"\e01b"}.icon-fire:before{content:"\e01c"}.icon-eyeglass:before{content:"\e01d"}.icon-envelope-open:before{content:"\e01e"}.icon-envelope-letter:before{content:"\e01f"}.icon-bell:before{content:"\e027"}.icon-badge:before{content:"\e028"}.icon-anchor:before{content:"\e029"}.icon-wallet:before{content:"\e02a"}.icon-vector:before{content:"\e02b"}.icon-speech:before{content:"\e02c"}.icon-puzzle:before{content:"\e02d"}.icon-printer:before{content:"\e02e"}.icon-present:before{content:"\e02f"}.icon-playlist:before{content:"\e030"}.icon-pin:before{content:"\e031"}.icon-picture:before{content:"\e032"}.icon-handbag:before{content:"\e035"}.icon-globe-alt:before{content:"\e036"}.icon-globe:before{content:"\e037"}.icon-folder-alt:before{content:"\e039"}.icon-folder:before{content:"\e089"}.icon-film:before{content:"\e03a"}.icon-feed:before{content:"\e03b"}.icon-drop:before{content:"\e03e"}.icon-drawer:before{content:"\e03f"}.icon-docs:before{content:"\e040"}.icon-doc:before{content:"\e085"}.icon-diamond:before{content:"\e043"}.icon-cup:before{content:"\e044"}.icon-calculator:before{content:"\e049"}.icon-bubbles:before{content:"\e04a"}.icon-briefcase:before{content:"\e04b"}.icon-book-open:before{content:"\e04c"}.icon-basket-loaded:before{content:"\e04d"}.icon-basket:before{content:"\e04e"}.icon-bag:before{content:"\e04f"}.icon-action-undo:before{content:"\e050"}.icon-action-redo:before{content:"\e051"}.icon-wrench:before{content:"\e052"}.icon-umbrella:before{content:"\e053"}.icon-trash:before{content:"\e054"}.icon-tag:before{content:"\e055"}.icon-support:before{content:"\e056"}.icon-frame:before{content:"\e038"}.icon-size-fullscreen:before{content:"\e057"}.icon-size-actual:before{content:"\e058"}.icon-shuffle:before{content:"\e059"}.icon-share-alt:before{content:"\e05a"}.icon-share:before{content:"\e05b"}.icon-rocket:before{content:"\e05c"}.icon-question:before{content:"\e05d"}.icon-pie-chart:before{content:"\e05e"}.icon-pencil:before{content:"\e05f"}.icon-note:before{content:"\e060"}.icon-loop:before{content:"\e064"}.icon-home:before{content:"\e069"}.icon-grid:before{content:"\e06a"}.icon-graph:before{content:"\e06b"}.icon-microphone:before{content:"\e063"}.icon-music-tone-alt:before{content:"\e061"}.icon-music-tone:before{content:"\e062"}.icon-earphones-alt:before{content:"\e03c"}.icon-earphones:before{content:"\e03d"}.icon-equalizer:before{content:"\e06c"}.icon-like:before{content:"\e068"}.icon-dislike:before{content:"\e06d"}.icon-control-start:before{content:"\e06f"}.icon-control-rewind:before{content:"\e070"}.icon-control-play:before{content:"\e071"}.icon-control-pause:before{content:"\e072"}.icon-control-forward:before{content:"\e073"}.icon-control-end:before{content:"\e074"}.icon-volume-1:before{content:"\e09f"}.icon-volume-2:before{content:"\e0a0"}.icon-volume-off:before{content:"\e0a1"}.icon-calendar:before{content:"\e075"}.icon-bulb:before{content:"\e076"}.icon-chart:before{content:"\e077"}.icon-ban:before{content:"\e07c"}.icon-bubble:before{content:"\e07d"}.icon-camrecorder:before{content:"\e07e"}.icon-camera:before{content:"\e07f"}.icon-cloud-download:before{content:"\e083"}.icon-cloud-upload:before{content:"\e084"}.icon-envelope:before{content:"\e086"}.icon-eye:before{content:"\e087"}.icon-flag:before{content:"\e088"}.icon-heart:before{content:"\e08a"}.icon-info:before{content:"\e08b"}.icon-key:before{content:"\e08c"}.icon-link:before{content:"\e08d"}.icon-lock:before{content:"\e08e"}.icon-lock-open:before{content:"\e08f"}.icon-magnifier:before{content:"\e090"}.icon-magnifier-add:before{content:"\e091"}.icon-magnifier-remove:before{content:"\e092"}.icon-paper-clip:before{content:"\e093"}.icon-paper-plane:before{content:"\e094"}.icon-power:before{content:"\e097"}.icon-refresh:before{content:"\e098"}.icon-reload:before{content:"\e099"}.icon-settings:before{content:"\e09a"}.icon-star:before{content:"\e09b"}.icon-symbol-female:before{content:"\e09c"}.icon-symbol-male:before{content:"\e09d"}.icon-target:before{content:"\e09e"}.icon-credit-card:before{content:"\e025"}.icon-paypal:before{content:"\e608"}.icon-social-tumblr:before{content:"\e00a"}.icon-social-twitter:before{content:"\e009"}.icon-social-facebook:before{content:"\e00b"}.icon-social-instagram:before{content:"\e609"}.icon-social-linkedin:before{content:"\e60a"}.icon-social-pinterest:before{content:"\e60b"}.icon-social-github:before{content:"\e60c"}.icon-social-google:before{content:"\e60d"}.icon-social-reddit:before{content:"\e60e"}.icon-social-skype:before{content:"\e60f"}.icon-social-dribbble:before{content:"\e00d"}.icon-social-behance:before{content:"\e610"}.icon-social-foursqare:before{content:"\e611"}.icon-social-soundcloud:before{content:"\e612"}.icon-social-spotify:before{content:"\e613"}.icon-social-stumbleupon:before{content:"\e614"}.icon-social-youtube:before{content:"\e008"}.icon-social-dropbox:before{content:"\e00c"}.icon-social-vkontakte:before{content:"\e618"}.icon-social-steam:before{content:"\e620"}/*# sourceMappingURL=simple-line-icons.min.css.map */ -------------------------------------------------------------------------------- /backend/core/static/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin-top: 70px; 3 | } 4 | 5 | label.required:after { 6 | content: ' *'; 7 | color: red; 8 | } 9 | 10 | .span-is-link { 11 | cursor: pointer; 12 | } 13 | 14 | .link { 15 | color: #007bff; 16 | } 17 | 18 | .ok { 19 | color: green; 20 | } 21 | 22 | .no { 23 | color: red; 24 | } 25 | 26 | tr.htmx-swapping td { 27 | opacity: 0; 28 | transition: opacity 0.5s ease-out; 29 | } 30 | 31 | .htmx-settling tr.deactivate td { 32 | background: lightcoral; 33 | } 34 | 35 | .htmx-settling tr.activate td { 36 | background: darkseagreen; 37 | } 38 | 39 | tr td { 40 | transition: all 1.2s; 41 | } 42 | 43 | .deactivate { 44 | text-decoration: line-through; 45 | } 46 | 47 | input[type=checkbox] { 48 | /* Double-sized Checkboxes */ 49 | -ms-transform: scale(2); 50 | -moz-transform: scale(2); 51 | -webkit-transform: scale(2); 52 | -o-transform: scale(2); 53 | padding: 10px; 54 | } -------------------------------------------------------------------------------- /backend/core/static/css/table.css: -------------------------------------------------------------------------------- 1 | .dt-panel { 2 | width: 100%; 3 | display: flex; 4 | align-items: center; 5 | } 6 | 7 | .dt-left-panel { 8 | width: 10%; 9 | flex-direction: row; 10 | align-items: center; 11 | } 12 | 13 | .dt-right-panel { 14 | width: 50%; 15 | margin-left: auto; 16 | align-items: flex-end; 17 | } 18 | 19 | .dt-total-panel { 20 | width: 20%; 21 | } 22 | 23 | .dt-pagination { 24 | display: flex; 25 | align-items: center; 26 | justify-content: flex-end; 27 | } -------------------------------------------------------------------------------- /backend/core/static/img/django-logo-negative.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-datatables-htmx/1abb4e0d2437cde0387a8409d6130de3d9818dc8/backend/core/static/img/django-logo-negative.png -------------------------------------------------------------------------------- /backend/core/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | {% load static %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Django 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% block css %}{% endblock css %} 26 | 27 | 28 | 29 | 30 |
31 | {% include "includes/nav.html" %} 32 | {% block content %}{% endblock content %} 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {% block js %}{% endblock js %} 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /backend/core/templates/includes/nav.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/core/templates/includes/pagination.html: -------------------------------------------------------------------------------- 1 | 2 | {% load url_replace %} 3 | 4 | 5 |
6 |
7 | 83 |
84 |
85 | -------------------------------------------------------------------------------- /backend/core/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block content %} 5 |
6 |

Django datatables htmx

7 |

Exemplo de datatables feito apenas com htmx.

8 |

Veja no Github.

9 |
10 | {% endblock content %} 11 | -------------------------------------------------------------------------------- /backend/core/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-datatables-htmx/1abb4e0d2437cde0387a8409d6130de3d9818dc8/backend/core/templatetags/__init__.py -------------------------------------------------------------------------------- /backend/core/templatetags/rows_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.simple_tag 7 | def first_line_count(*args, **kwargs): 8 | ''' 9 | Calcula o primeiro valor do número de linhas. 10 | ''' 11 | pg_number = kwargs.get('pg_number') 12 | rows_per_page = int(kwargs.get('rows_per_page')) 13 | return pg_number * rows_per_page - rows_per_page + 1 14 | 15 | 16 | @register.simple_tag 17 | def last_line_count(*args, **kwargs): 18 | ''' 19 | Calcula o último valor do número de linhas. 20 | ''' 21 | pg_number = kwargs.get('pg_number') 22 | rows_per_page = int(kwargs.get('rows_per_page')) 23 | total_items = kwargs.get('total_items') 24 | 25 | last_line_count = pg_number * rows_per_page 26 | 27 | if total_items < last_line_count: 28 | return total_items 29 | return last_line_count 30 | -------------------------------------------------------------------------------- /backend/core/templatetags/url_replace.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.simple_tag(takes_context=True) 7 | def url_replace(context, **kwargs): 8 | query = context['request'].GET.copy() 9 | query.pop('rows_per_page', None) 10 | query.pop('page', None) 11 | query.pop('search', None) 12 | query.pop('sort_by', None) 13 | query.update(kwargs) 14 | return query.urlencode() 15 | -------------------------------------------------------------------------------- /backend/core/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from backend.core import views as v 4 | 5 | app_name = 'core' 6 | 7 | 8 | urlpatterns = [ 9 | path('', v.index, name='index'), 10 | ] 11 | -------------------------------------------------------------------------------- /backend/core/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | 4 | def index(request): 5 | template_name = 'index.html' 6 | return render(request, template_name) 7 | -------------------------------------------------------------------------------- /backend/expense/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-datatables-htmx/1abb4e0d2437cde0387a8409d6130de3d9818dc8/backend/expense/__init__.py -------------------------------------------------------------------------------- /backend/expense/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Expense 4 | 5 | 6 | @admin.register(Expense) 7 | class ExpenseAdmin(admin.ModelAdmin): 8 | list_display = ('__str__', 'value', 'payment_date', 'paid') 9 | search_fields = ('description',) 10 | list_filter = ('paid',) 11 | date_hierarchy = 'payment_date' 12 | -------------------------------------------------------------------------------- /backend/expense/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ExpenseConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'backend.expense' 7 | -------------------------------------------------------------------------------- /backend/expense/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2021-12-25 06:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Expense', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('created', models.DateTimeField(auto_now_add=True, verbose_name='criado em')), 19 | ('modified', models.DateTimeField(auto_now=True, verbose_name='modificado em')), 20 | ('description', models.CharField(max_length=50, verbose_name='descrição')), 21 | ('payment_date', models.DateField(blank=True, null=True, verbose_name='data de pagamento')), 22 | ('value', models.DecimalField(decimal_places=2, max_digits=7, verbose_name='valor')), 23 | ('paid', models.BooleanField(default=False, verbose_name='pago')), 24 | ], 25 | options={ 26 | 'verbose_name': 'despesa', 27 | 'verbose_name_plural': 'despesas', 28 | 'ordering': ('-payment_date',), 29 | }, 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /backend/expense/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-datatables-htmx/1abb4e0d2437cde0387a8409d6130de3d9818dc8/backend/expense/migrations/__init__.py -------------------------------------------------------------------------------- /backend/expense/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from backend.core.models import TimeStampedModel 4 | 5 | 6 | class Expense(TimeStampedModel): 7 | description = models.CharField('descrição', max_length=50) 8 | payment_date = models.DateField('data de pagamento', null=True, blank=True) 9 | value = models.DecimalField('valor', max_digits=7, decimal_places=2) 10 | paid = models.BooleanField('pago', default=False) 11 | 12 | class Meta: 13 | ordering = ('id',) 14 | verbose_name = 'despesa' 15 | verbose_name_plural = 'despesas' 16 | 17 | def __str__(self): 18 | return self.description 19 | -------------------------------------------------------------------------------- /backend/expense/templates/expense/expense_content.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ object.id }} 4 | {{ object.description }} 5 | {{ object.value }} 6 | {{ object.payment_date|date:'d/m/Y'|default:'---' }} 7 | 8 | -------------------------------------------------------------------------------- /backend/expense/templates/expense/expense_list.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | {% load rows_tags %} 4 | {% load url_replace %} 5 | 6 | {% block content %} 7 | 8 |
9 |
10 |
11 | 31 | Linhas por página 32 |
33 | 34 |
35 |
36 | 45 | Limpar 49 |
50 |
51 |
52 | 53 | 54 | 55 | 56 | 57 | 61 | 65 | 69 | 70 | 71 | 72 | {% include "./expense_table.html" %} 73 | 74 |
ID 58 | {% include "../includes/ordering_field.html" with url='/expense/' field=sort_by.description target='#example' %} 59 | {% include "../includes/ordering_icon.html" with order=sort_by.description.ordering %} 60 | 62 | {% include "../includes/ordering_field.html" with url='/expense/' field=sort_by.value target='#example' %} 63 | {% include "../includes/ordering_icon.html" with order=sort_by.value.ordering %} 64 | 66 | {% include "../includes/ordering_field.html" with url='/expense/' field=sort_by.payment_date target='#example' %} 67 | {% include "../includes/ordering_icon.html" with order=sort_by.payment_date.ordering %} 68 |
75 | 76 |
77 |
78 | 79 | 80 | Linhas {% first_line_count rows_per_page=rows_per_page pg_number=page_obj.number %} 81 | a {% last_line_count rows_per_page=rows_per_page pg_number=page_obj.number total_items=total_items %} 82 | de {{ page_obj.paginator.count }} 83 |
84 | 85 |
86 |
87 | {% include "includes/pagination.html" %} 88 |
89 |
90 |
91 |
92 | 93 | {% endblock content %} 94 | 95 | {% block js %} 96 | 97 | 103 | 104 | {% endblock js %} 105 | -------------------------------------------------------------------------------- /backend/expense/templates/expense/expense_table.html: -------------------------------------------------------------------------------- 1 | 2 | {% for object in object_list %} 3 | {% include "./expense_content.html" %} 4 | {% endfor %} 5 | -------------------------------------------------------------------------------- /backend/expense/templates/includes/ordering_field.html: -------------------------------------------------------------------------------- 1 | {% load url_replace %} 2 | 3 | 8 | {{ field.label }} 9 | 10 | -------------------------------------------------------------------------------- /backend/expense/templates/includes/ordering_icon.html: -------------------------------------------------------------------------------- 1 | 2 | {% if order == ordering %} 3 | {% if '-' in order %} 4 | 5 | {% else %} 6 | 7 | {% endif %} 8 | {% else %} 9 | 10 | {% endif %} 11 | 12 | -------------------------------------------------------------------------------- /backend/expense/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from backend.expense import views as v 4 | 5 | app_name = 'expense' 6 | 7 | 8 | urlpatterns = [ 9 | path('', v.ExpenseListView.as_view(), name='expense_list'), 10 | ] 11 | -------------------------------------------------------------------------------- /backend/expense/views.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Q 2 | from django.views.generic import ListView 3 | 4 | from .models import Expense 5 | 6 | 7 | class DatatablesMixin: 8 | paginate = 10 9 | rows_per_pages = (5, 10, 20, 50, 100) 10 | 11 | def get_context_data(self, **kwargs): 12 | context = super().get_context_data(**kwargs) 13 | context['rows_per_pages'] = self.rows_per_pages 14 | 15 | # Busca 16 | search = self.request.GET.get('search') 17 | if search: 18 | context['search'] = search 19 | 20 | # Ordenação 21 | sort_by = self.request.GET.get('sort_by') 22 | if sort_by: 23 | if '-' in sort_by: 24 | _sort_by = sort_by.replace('-', '') 25 | # key é o nome do campo a ser ordenado. 26 | key = _sort_by 27 | else: 28 | # Muda a ordenação (como se fosse toggle). 29 | _sort_by = f'-{sort_by}' 30 | key = sort_by 31 | else: 32 | # Pega o nome do primeiro campo de ordenação do model. 33 | key = self.model._meta.ordering[0].replace('-', '') 34 | _sort_by = key 35 | 36 | # Atualiza o dicionário procurando pelo campo de ordenação. 37 | self.sort_by[key]['ordering'] = _sort_by 38 | context['sort_by'] = self.sort_by 39 | context['ordering'] = _sort_by 40 | 41 | # Linhas por página 42 | rows_per_page = self.request.GET.get('rows_per_page') 43 | if rows_per_page: 44 | context['rows_per_page'] = rows_per_page 45 | else: 46 | context['rows_per_page'] = self.paginate 47 | 48 | # Total de itens 49 | total_items = self.model.objects.values_list('id', flat=True).count() 50 | context['total_items'] = total_items 51 | 52 | return context 53 | 54 | def get_paginate_by(self, queryset): 55 | rows_per_page = self.request.GET.get('rows_per_page') 56 | if rows_per_page: 57 | return rows_per_page 58 | return self.paginate 59 | 60 | 61 | class ExpenseSearchMixin: 62 | ''' 63 | Campo de busca 64 | ''' 65 | 66 | def get_queryset(self): 67 | queryset = super().get_queryset() 68 | search = self.request.GET.get('search') 69 | 70 | if search: 71 | return queryset.filter( 72 | Q(description__icontains=search) 73 | ) 74 | return queryset 75 | 76 | 77 | class ExpenseSortMixin: 78 | ''' 79 | Ordenação 80 | ''' 81 | 82 | def __init__(self): 83 | self.sort_by = { 84 | 'id': { 85 | 'label': self.model.id.field.verbose_name.capitalize(), 86 | 'ordering': self.model.id.field.name, 87 | }, 88 | 'description': { 89 | 'label': self.model.description.field.verbose_name.capitalize(), 90 | 'ordering': self.model.description.field.name, 91 | }, 92 | 'payment_date': { 93 | 'label': self.model.payment_date.field.verbose_name.capitalize(), 94 | 'ordering': self.model.payment_date.field.name, 95 | }, 96 | 'value': { 97 | 'label': self.model.value.field.verbose_name.capitalize(), 98 | 'ordering': self.model.value.field.name, 99 | }, 100 | } 101 | 102 | def get_context_data(self, **kwargs): 103 | context = super().get_context_data(**kwargs) 104 | context['sort_by'] = self.sort_by 105 | return context 106 | 107 | def get_queryset(self): 108 | queryset = super().get_queryset() 109 | sort_by = self.request.GET.get('sort_by') 110 | 111 | if sort_by: 112 | return queryset.order_by(sort_by) 113 | return queryset 114 | 115 | 116 | class ExpenseListView(DatatablesMixin, ExpenseSearchMixin, ExpenseSortMixin, ListView): 117 | model = Expense 118 | template_name = 'expense/expense_list.html' 119 | -------------------------------------------------------------------------------- /backend/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for backend project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.0. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.0/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | from decouple import Csv, config 16 | from dj_database_url import parse as dburl 17 | 18 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 19 | BASE_DIR = Path(__file__).resolve().parent.parent 20 | 21 | 22 | # Quick-start development settings - unsuitable for production 23 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ 24 | 25 | # SECURITY WARNING: keep the secret key used in production secret! 26 | SECRET_KEY = config('SECRET_KEY') 27 | 28 | # SECURITY WARNING: don't run with debug turned on in production! 29 | DEBUG = config('DEBUG', default=False, cast=bool) 30 | 31 | ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=[], cast=Csv()) 32 | 33 | 34 | # Application definition 35 | 36 | INSTALLED_APPS = [ 37 | 'django.contrib.admin', 38 | 'django.contrib.auth', 39 | 'django.contrib.contenttypes', 40 | 'django.contrib.sessions', 41 | 'django.contrib.messages', 42 | 'django.contrib.staticfiles', 43 | # 3rd apps 44 | 'django_extensions', 45 | 'widget_tweaks', 46 | 'django_seed', 47 | # my apps 48 | 'backend.core.apps.CoreConfig', 49 | 'backend.expense.apps.ExpenseConfig', 50 | ] 51 | 52 | MIDDLEWARE = [ 53 | 'django.middleware.security.SecurityMiddleware', 54 | 'django.contrib.sessions.middleware.SessionMiddleware', 55 | 'django.middleware.common.CommonMiddleware', 56 | 'django.middleware.csrf.CsrfViewMiddleware', 57 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 58 | 'django.contrib.messages.middleware.MessageMiddleware', 59 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 60 | ] 61 | 62 | ROOT_URLCONF = 'backend.urls' 63 | 64 | TEMPLATES = [ 65 | { 66 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 67 | 'DIRS': [], 68 | 'APP_DIRS': True, 69 | 'OPTIONS': { 70 | 'context_processors': [ 71 | 'django.template.context_processors.debug', 72 | 'django.template.context_processors.request', 73 | 'django.contrib.auth.context_processors.auth', 74 | 'django.contrib.messages.context_processors.messages', 75 | ], 76 | }, 77 | }, 78 | ] 79 | 80 | WSGI_APPLICATION = 'backend.wsgi.application' 81 | 82 | 83 | # Database 84 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases 85 | 86 | default_dburl = 'sqlite:///' + str(BASE_DIR / 'db.sqlite3') 87 | DATABASES = { 88 | 'default': config('DATABASE_URL', default=default_dburl, cast=dburl), 89 | } 90 | 91 | 92 | # Password validation 93 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators 94 | 95 | AUTH_PASSWORD_VALIDATORS = [ 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 107 | }, 108 | ] 109 | 110 | 111 | # Internationalization 112 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ 113 | 114 | LANGUAGE_CODE = 'pt-br' # 'en-us' 115 | 116 | TIME_ZONE = 'America/Sao_Paulo' # 'UTC' 117 | 118 | USE_I18N = True 119 | 120 | USE_L10N = True 121 | 122 | USE_TZ = True 123 | 124 | USE_THOUSAND_SEPARATOR = True 125 | 126 | DECIMAL_SEPARATOR = ',' 127 | 128 | 129 | # Static files (CSS, JavaScript, Images) 130 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ 131 | 132 | STATIC_URL = '/static/' 133 | STATIC_ROOT = BASE_DIR.joinpath('staticfiles') 134 | 135 | # Default primary key field type 136 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field 137 | 138 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 139 | 140 | 141 | LOGIN_URL = '/admin/login/' 142 | LOGIN_REDIRECT_URL = 'core:index' 143 | # LOGOUT_REDIRECT_URL = 'core:index' 144 | -------------------------------------------------------------------------------- /backend/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path 3 | 4 | urlpatterns = [ 5 | path('', include('backend.core.urls', namespace='core')), 6 | path('expense/', include('backend.expense.urls', namespace='expense')), 7 | path('admin/', admin.site.urls), 8 | ] 9 | -------------------------------------------------------------------------------- /backend/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for backend project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /contrib/env_gen.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python SECRET_KEY generator. 3 | """ 4 | import random 5 | 6 | chars = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!?@#$%^&*()" 7 | size = 50 8 | secret_key = "".join(random.sample(chars, size)) 9 | 10 | chars = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!?@#$%_" 11 | size = 20 12 | password = "".join(random.sample(chars, size)) 13 | 14 | CONFIG_STRING = """ 15 | DEBUG=True 16 | SECRET_KEY=%s 17 | ALLOWED_HOSTS=127.0.0.1,.localhost,0.0.0.0 18 | 19 | #DATABASE_URL=postgres://USER:PASSWORD@HOST:PORT/NAME 20 | #POSTGRES_DB= 21 | #POSTGRES_USER= 22 | #POSTGRES_PASSWORD=%s 23 | #DB_HOST=localhost 24 | 25 | #DEFAULT_FROM_EMAIL= 26 | #EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend 27 | #EMAIL_HOST=localhost 28 | #EMAIL_PORT= 29 | #EMAIL_HOST_USER= 30 | #EMAIL_HOST_PASSWORD= 31 | #EMAIL_USE_TLS=True 32 | """.strip() % (secret_key, password) 33 | 34 | # Writing our configuration file to '.env' 35 | with open('.env', 'w') as configfile: 36 | configfile.write(CONFIG_STRING) 37 | 38 | print('Success!') 39 | print('Type: cat .env') 40 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dj-database-url==0.5.0 2 | django-extensions==3.1.5 3 | django-seed==0.3.1 4 | django-widget-tweaks==1.4.9 5 | Django==4.0 6 | djhtml==1.4.11 7 | isort==5.10.1 8 | psycopg2-binary 9 | python-decouple==3.5 10 | --------------------------------------------------------------------------------