├── .gitignore ├── README.md ├── backend ├── __init__.py ├── asgi.py ├── bookstore │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── templates │ │ └── bookstore │ │ │ ├── book_list.html │ │ │ ├── book_table.html │ │ │ ├── hx │ │ │ ├── book_detail_hx.html │ │ │ ├── book_form_hx.html │ │ │ ├── book_result_hx.html │ │ │ └── book_update_form_hx.html │ │ │ └── includes │ │ │ ├── add_modal.html │ │ │ ├── detail_modal.html │ │ │ └── update_modal.html │ ├── tests.py │ ├── urls.py │ └── views.py ├── core │ ├── __init__.py │ ├── apps.py │ ├── models.py │ ├── static │ │ ├── css │ │ │ ├── form.css │ │ │ └── style.css │ │ └── img │ │ │ └── django-logo-negative.png │ ├── templates │ │ ├── base.html │ │ ├── includes │ │ │ └── nav.html │ │ └── index.html │ ├── urls.py │ └── views.py ├── expense │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── templates │ │ └── expense │ │ │ ├── expense_client.html │ │ │ ├── expense_list.html │ │ │ ├── expense_table.html │ │ │ └── hx │ │ │ ├── expense_detail_hx.html │ │ │ └── expense_hx.html │ ├── urls.py │ └── views.py ├── product │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── templates │ │ └── product │ │ │ ├── hx │ │ │ ├── category_modal_form_hx.html │ │ │ └── product_result_hx.html │ │ │ ├── includes │ │ │ └── add_modal.html │ │ │ ├── product_list.html │ │ │ └── product_table.html │ ├── tests.py │ ├── urls.py │ └── views.py ├── settings.py ├── state │ ├── __init__.py │ ├── apps.py │ ├── states.py │ ├── templates │ │ └── state │ │ │ ├── hx │ │ │ ├── state_hx.html │ │ │ └── uf_hx.html │ │ │ └── state_list.html │ ├── urls.py │ └── views.py ├── urls.py └── wsgi.py ├── contrib └── env_gen.py ├── db.json ├── img ├── 01_expense_add.png ├── 02_expense_bulk_update.png ├── 03_expense_update.png ├── 04_expense_delete.png ├── a02_combobox.png ├── a03_tabela.png ├── expense_base.png ├── htmx-2021-07-22-0947.excalidraw └── htmx.png ├── index.html ├── 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-htmx-tutorial 2 | 3 | Tutorial sobre como trabalhar com [Django](https://www.djangoproject.com/) e [htmx](https://htmx.org/). 4 | 5 | ![htmx.png](img/htmx.png) 6 | 7 | ## Este projeto foi feito com: 8 | 9 | * [Python 3.9.6](https://www.python.org/) 10 | * [Django 3.2.*](https://www.djangoproject.com/) 11 | * [htmx](https://htmx.org/) 12 | 13 | ## Como rodar o projeto? 14 | 15 | * Clone esse repositório. 16 | * Crie um virtualenv com Python 3. 17 | * Ative o virtualenv. 18 | * Instale as dependências. 19 | * Rode as migrações. 20 | 21 | ``` 22 | git clone https://github.com/rg3915/django-htmx-tutorial.git 23 | cd django-htmx-tutorial 24 | python -m venv .venv 25 | source .venv/bin/activate 26 | pip install -r requirements.txt 27 | python contrib/env_gen.py 28 | python manage.py migrate 29 | python manage.py createsuperuser --username="admin" --email="" 30 | python manage.py runserver 31 | ``` 32 | 33 | ## Exemplos 34 | 35 | * [Filtrar várias tabelas com um clique](#filtrar-v%C3%A1rias-tabelas-com-um-clique) 36 | 37 | 38 | 39 | 40 | * [Filtrar com dropdowns dependentes](#filtrar-com-dropdowns-dependentes) 41 | 42 | 43 | 44 | 45 | * [Adicionar itens](#adicionar-itens) 46 | 47 | 48 | 49 | 50 | * [Pagar (editar) vários itens (Bulk Update)](#pagar-editar-vários-itens-bulk-update) 51 | 52 | 53 | 54 | 55 | * [Editar um item](#editar-um-item) 56 | 57 | 58 | 59 | 60 | * [Deletar um item](#deletar-um-item) 61 | 62 | 63 | 64 | 65 | * [client-side-templates](#client-side-templates) 66 | 67 | * [Bookstore (modal)](#bookstore) 68 | 69 | * [Like e unlike](#like-e-unlike) 70 | 71 | * [Criar categoria](#criar-uma-nova-categoria) 72 | 73 | * [Trocar de categoria](#trocar-a-categoria) 74 | 75 | 76 | ## Passo a passo 77 | 78 | ### Clonando o projeto base 79 | 80 | ``` 81 | git clone https://github.com/rg3915/django-htmx-tutorial.git 82 | cd django-htmx-tutorial 83 | git checkout passo-a-passo 84 | 85 | python -m venv .venv 86 | source .venv/bin/activate 87 | 88 | pip install -U pip 89 | pip install -r requirements.txt 90 | pip install ipdb 91 | 92 | python contrib/env_gen.py 93 | 94 | python manage.py migrate 95 | python manage.py createsuperuser --username="admin" --email="" 96 | ``` 97 | 98 | Vamos editar: 99 | 100 | * base.html 101 | * nav.html 102 | * index.html 103 | 104 | 105 | Em `base.html` escreva 106 | 107 | ```html 108 | 109 | 110 | 111 | 112 | ``` 113 | 114 | Em `nav.html` escreva 115 | 116 | ```html 117 | 120 | 123 | 126 | ``` 127 | 128 | Corrija o link em `index.html` 129 | 130 | ```html 131 | Estados 132 | ``` 133 | 134 | 135 | ## Exemplos 136 | 137 | ### Filtrar várias tabelas com um clique 138 | 139 | ![a03_tabela.png](img/a03_tabela.png) 140 | 141 | Considere a app `state`. 142 | 143 | 144 | Vamos editar: 145 | 146 | * views.py 147 | * urls.py 148 | * state_list.html 149 | * hx/state_hx.html 150 | 151 | 152 | Em `state/views.py` escreva 153 | 154 | ```python 155 | # state/views.py 156 | from django.shortcuts import render 157 | 158 | from .states import states 159 | 160 | 161 | def state_list(request): 162 | template_name = 'state/state_list.html' 163 | 164 | regions = ( 165 | ('n', 'Norte'), 166 | ('ne', 'Nordeste'), 167 | ('s', 'Sul'), 168 | ('se', 'Sudeste'), 169 | ('co', 'Centro-Oeste'), 170 | ) 171 | 172 | context = {'regions': regions} 173 | return render(request, template_name, context) 174 | ``` 175 | 176 | Em `state/urls.py` escreva 177 | 178 | ```python 179 | # state/urls.py 180 | from django.urls import path 181 | 182 | from backend.state import views as v 183 | 184 | app_name = 'state' 185 | 186 | 187 | urlpatterns = [ 188 | path('', v.state_list, name='state_list'), 189 | path('result/', v.state_result, name='state_result'), 190 | ] 191 | 192 | ``` 193 | 194 | Crie as pastas 195 | 196 | ``` 197 | mkdir -p state/templates/state/hx 198 | ``` 199 | 200 | 201 | Escreva o template 202 | 203 | `touch state/templates/state/state_list.html` 204 | 205 | ```html 206 | 207 | {% extends "base.html" %} 208 | 209 | {% block content %} 210 |

Regiões e Estados do Brasil

211 | 212 |

Filtrando várias tabelas com um clique

213 | 214 |
215 | 216 |
217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | {% for region in regions %} 225 | 229 | 232 | 233 | {% endfor %} 234 | 235 |
Região
230 | {{ region.1 }} 231 |
236 |
237 | 238 |
239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 |
Estados
249 |
250 | 251 |
252 | {% endblock content %} 253 | ``` 254 | 255 | Em `state/views.py` escreva 256 | 257 | ```python 258 | # state/views.py 259 | def get_states(region): 260 | return [state for state in states.get(region).items()] 261 | 262 | def state_result(request): 263 | template_name = 'state/hx/state_hx.html' 264 | region = request.GET.get('region') 265 | 266 | ufs = { 267 | 'n': get_states('Norte'), 268 | 'ne': get_states('Nordeste'), 269 | 's': get_states('Sul'), 270 | 'se': get_states('Sudeste'), 271 | 'co': get_states('Centro-Oeste'), 272 | } 273 | 274 | context = {'ufs': ufs[region]} 275 | return render(request, template_name, context) 276 | ``` 277 | 278 | Escreva o template 279 | 280 | `touch state/templates/state/hx/state_hx.html` 281 | 282 | ```html 283 | 284 | {% for uf in ufs %} 285 | 286 | {{ uf.1 }} 287 | 288 | {% endfor %} 289 | ``` 290 | 291 | Descomente em `urls.py` 292 | 293 | ``` 294 | path('state/', include('backend.state.urls', namespace='state')), 295 | ``` 296 | 297 | 298 | --- 299 | 300 | ### Filtrar com dropdowns dependentes 301 | 302 | ![a02_combobox.png](img/a02_combobox.png) 303 | 304 | Vamos editar: 305 | 306 | * urls.py 307 | * views.py 308 | * hx/uf_hx.html 309 | * state_list.html 310 | 311 | 312 | Edite `state/urls.py` 313 | 314 | ```python 315 | # state/urls.py 316 | ... 317 | path('uf/', v.uf_list, name='uf_list'), 318 | ... 319 | ``` 320 | 321 | Edite `state/views.py` 322 | 323 | ```python 324 | # state/views.py 325 | def uf_list(request): 326 | template_name = 'state/hx/uf_hx.html' 327 | region = request.GET.get('region') 328 | 329 | ufs = { 330 | 'n': get_states('Norte'), 331 | 'ne': get_states('Nordeste'), 332 | 's': get_states('Sul'), 333 | 'se': get_states('Sudeste'), 334 | 'co': get_states('Centro-Oeste'), 335 | } 336 | 337 | context = {'ufs': ufs[region]} 338 | return render(request, template_name, context) 339 | ``` 340 | 341 | Edite 342 | 343 | `touch state/templates/state/hx/uf_hx.html` 344 | 345 | ```html 346 | 347 | {% for uf in ufs %} 348 | 349 | {% endfor %} 350 | ``` 351 | 352 | Edite `state/templates/state/state_list.html` 353 | 354 | ```html 355 |

Filtro com dropdowns dependentes

356 | 357 |
358 | 359 |
360 | 361 | 372 |
373 | 374 |
375 | 376 | 383 |
384 | 385 |
386 | 387 |
388 | 389 | ... 390 | ``` 391 | 392 | --- 393 | 394 | ### A base para despesas 395 | 396 | Considere o desenho a seguir: 397 | 398 | ![expense_base.png](img/expense_base.png) 399 | 400 | `expense_hx.html` será inserido em 401 | `expense_table.hmtl`, que por sua vez 402 | será inserido em `expense_list.html`. 403 | 404 | Sendo que `expense_hx.html` será repetido várias vezes por causa do laço de repetição em `expense_table.hmtl`. 405 | 406 | --- 407 | 408 | ### Adicionar itens 409 | 410 | ![01_expense_add.png](img/01_expense_add.png) 411 | 412 | Vamos editar: 413 | 414 | * models.py 415 | * admin.py 416 | * forms.py 417 | * views.py 418 | * urls.py 419 | * expense_list.html 420 | * expense_table.html 421 | * hx/expense_hx.html 422 | * nav.html 423 | * index.html 424 | 425 | 426 | Escreva o `expense/models.py` 427 | 428 | ```python 429 | # expense/models.py 430 | from django.db import models 431 | 432 | from backend.core.models import TimeStampedModel 433 | 434 | 435 | class Expense(TimeStampedModel): 436 | description = models.CharField('descrição', max_length=30) 437 | value = models.DecimalField('valor', max_digits=7, decimal_places=2) 438 | paid = models.BooleanField('pago', default=False) 439 | 440 | class Meta: 441 | ordering = ('description',) 442 | verbose_name = 'despesa' 443 | verbose_name_plural = 'despesas' 444 | 445 | def __str__(self): 446 | return self.description 447 | ``` 448 | 449 | Escreva o `expense/admin.py` 450 | 451 | ```python 452 | # expense/admin.py 453 | from django.contrib import admin 454 | 455 | from .models import Expense 456 | 457 | 458 | @admin.register(Expense) 459 | class ExpenseAdmin(admin.ModelAdmin): 460 | list_display = ('__str__', 'value', 'paid') 461 | search_fields = ('description',) 462 | list_filter = ('paid',) 463 | ``` 464 | 465 | Escreva 466 | 467 | `touch expense/forms.py` 468 | 469 | ```python 470 | # expense/forms.py 471 | from django import forms 472 | 473 | from .models import Expense 474 | 475 | 476 | class ExpenseForm(forms.ModelForm): 477 | required_css_class = 'required' 478 | 479 | class Meta: 480 | model = Expense 481 | fields = ('description', 'value') 482 | widgets = { 483 | 'description': forms.TextInput(attrs={'placeholder': 'Descrição', 'autofocus': True}), 484 | 'value': forms.NumberInput(attrs={'placeholder': 'Valor'}), 485 | } 486 | 487 | def __init__(self, *args, **kwargs): 488 | super(ExpenseForm, self).__init__(*args, **kwargs) 489 | for field_name, field in self.fields.items(): 490 | field.widget.attrs['class'] = 'form-control' 491 | ``` 492 | 493 | Escreva o `expense/views.py` 494 | 495 | ```python 496 | # expense/views.py 497 | from django.http import JsonResponse 498 | from django.shortcuts import render 499 | from django.views.decorators.http import require_http_methods 500 | 501 | from .forms import ExpenseForm 502 | from .models import Expense 503 | 504 | 505 | def expense_list(request): 506 | template_name = 'expense/expense_list.html' 507 | form = ExpenseForm(request.POST or None) 508 | 509 | expenses = Expense.objects.all() 510 | 511 | context = {'object_list': expenses, 'form': form} 512 | return render(request, template_name, context) 513 | 514 | 515 | @require_http_methods(['POST']) 516 | def expense_create(request): 517 | form = ExpenseForm(request.POST or None) 518 | 519 | if form.is_valid(): 520 | expense = form.save() 521 | 522 | context = {'object': expense} 523 | return render(request, 'expense/hx/expense_hx.html', context) 524 | ``` 525 | 526 | Escreva o `expense/urls.py` 527 | 528 | ```python 529 | # expense/urls.py 530 | from django.urls import path 531 | 532 | from backend.expense import views as v 533 | 534 | app_name = 'expense' 535 | 536 | 537 | urlpatterns = [ 538 | path('', v.expense_list, name='expense_list'), 539 | path('create/', v.expense_create, name='expense_create'), 540 | ] 541 | ``` 542 | 543 | Crie as pastas 544 | 545 | ``` 546 | mkdir -p expense/templates/expense 547 | ``` 548 | 549 | 550 | Escreva 551 | 552 | `touch expense/templates/expense/expense_list.html` 553 | 554 | 555 | ```html 556 | 557 | {% extends "base.html" %} 558 | 559 | {% block content %} 560 |

Lista de Despesas

561 |
562 |
563 |
570 | {% csrf_token %} 571 | {% for field in form %} 572 |
573 | {{ field }} 574 | {{ field.errors }} 575 | {% if field.help_text %} 576 | {{ field.help_text|safe }} 577 | {% endif %} 578 |
579 | {% endfor %} 580 |
581 | 585 |
586 |
587 |
588 |
589 | 590 |
594 |
595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | {% include "./expense_table.html" %} 607 | 608 |
DescriçãoValorPagoAções
609 |
610 |
611 | 612 | {% endblock content %} 613 | 614 | {% block js %} 615 | 620 | {% endblock js %} 621 | ``` 622 | 623 | Escreva 624 | 625 | `touch expense/templates/expense/expense_table.html` 626 | 627 | ```html 628 | 629 | {% for object in object_list %} 630 | {% include "./hx/expense_hx.html" %} 631 | {% endfor %} 632 | ``` 633 | 634 | Escreva 635 | 636 | `touch expense/templates/expense/hx/expense_hx.html` 637 | 638 | ```html 639 | 640 | 645 | 646 | 651 | 652 | {{ object.description }} 653 | {{ object.value }} 654 | 655 | {% if object.paid %} 656 | 657 | 658 | 659 | {% else %} 660 | 661 | 662 | 663 | {% endif %} 664 | 665 | 666 | ``` 667 | 668 | Edite `nav.html` 669 | 670 | ```html 671 | Despesas 672 | ``` 673 | 674 | Edite `index.html` 675 | 676 | ```html 677 |

Despesas CRUD com SPA

678 | ``` 679 | 680 | Descomente `urls.py` 681 | 682 | ```python 683 | path('expense/', include('backend.expense.urls', namespace='expense')), 684 | ``` 685 | 686 | 687 | --- 688 | 689 | ### Pagar (editar) vários itens (Bulk Update) 690 | 691 | ![02_expense_bulk_update.png](img/02_expense_bulk_update.png) 692 | 693 | Vamos editar: 694 | 695 | * views.py 696 | * urls.py 697 | * expense_list.html 698 | 699 | 700 | Escreva o `expense/views.py` 701 | 702 | ```python 703 | @require_http_methods(['POST']) 704 | def expense_paid(request): 705 | ids = request.POST.getlist('ids') 706 | 707 | # Edita as despesas selecionadas. 708 | Expense.objects.filter(id__in=ids).update(paid=True) 709 | 710 | # Retorna todas as despesas novamente. 711 | expenses = Expense.objects.all() 712 | 713 | context = {'object_list': expenses} 714 | return render(request, 'expense/expense_table.html', context) 715 | 716 | 717 | @require_http_methods(['POST']) 718 | def expense_no_paid(request): 719 | ids = request.POST.getlist('ids') 720 | 721 | # Edita as despesas selecionadas. 722 | Expense.objects.filter(id__in=ids).update(paid=False) 723 | 724 | # Retorna todas as despesas novamente. 725 | expenses = Expense.objects.all() 726 | 727 | context = {'object_list': expenses} 728 | return render(request, 'expense/expense_table.html', context) 729 | ``` 730 | 731 | Escreva o `expense/urls.py` 732 | 733 | ```python 734 | path('expense/paid/', v.expense_paid, name='expense_paid'), 735 | path('expense/no-paid/', v.expense_no_paid, name='expense_no_paid'), 736 | ``` 737 | 738 | Escreva o `expense/expense_list.html` 739 | 740 | ```html 741 | 742 |
747 | Pago 751 | Não Pago 755 | Bulk update 756 |
757 | ``` 758 | 759 | --- 760 | 761 | ### Editar um item 762 | 763 | ![03_expense_update.png](img/03_expense_update.png) 764 | 765 | Vamos editar: 766 | 767 | * views.py 768 | * urls.py 769 | * hx/expense_hx.html 770 | * hx/expense_detail.html 771 | 772 | 773 | Escreva o `expense/views.py` 774 | 775 | ```python 776 | # expense/views.py 777 | def expense_detail(request, pk): 778 | template_name = 'expense/hx/expense_detail.html' 779 | obj = Expense.objects.get(pk=pk) 780 | form = ExpenseForm(request.POST or None, instance=obj) 781 | 782 | context = {'object': obj, 'form': form} 783 | return render(request, template_name, context) 784 | 785 | 786 | def expense_update(request, pk): 787 | template_name = 'expense/hx/expense_hx.html' 788 | obj = Expense.objects.get(pk=pk) 789 | form = ExpenseForm(request.POST or None, instance=obj) 790 | context = {'object': obj} 791 | 792 | if request.method == 'POST': 793 | if form.is_valid(): 794 | form.save() 795 | 796 | return render(request, template_name, context) 797 | ``` 798 | 799 | 800 | Escreva o `expense/urls.py` 801 | 802 | ```python 803 | # expense/urls.py 804 | path('/', v.expense_detail, name='expense_detail'), 805 | path('/update/', v.expense_update, name='expense_update'), 806 | ``` 807 | 808 | Escreva o `expense/hx/expense_hx.html` 809 | 810 | ```html 811 | 812 | 813 | 814 | 815 | 816 | 817 | ``` 818 | 819 | 820 | Escreva 821 | 822 | `touch expense/templates/expense/hx/expense_detail.html` 823 | 824 | ```html 825 | 826 | 827 | 828 | {{ form.description }} 829 | {{ form.value }} 830 | 831 | 832 | 841 | 849 | 850 | 851 | ``` 852 | 853 | --- 854 | 855 | ### Deletar um item 856 | 857 | ![04_expense_delete.png](img/04_expense_delete.png) 858 | 859 | Vamos editar: 860 | 861 | * views.py 862 | * urls.py 863 | * hx/expense_hx.html 864 | 865 | 866 | Escreva o `expense/views.py` 867 | 868 | ```python 869 | # expense/views.py 870 | @require_http_methods(['DELETE']) 871 | def expense_delete(request, pk): 872 | obj = Expense.objects.get(pk=pk) 873 | obj.delete() 874 | return render(request, 'expense/expense_table.html') 875 | ``` 876 | 877 | Escreva o `expense/urls.py` 878 | 879 | ```python 880 | # expense/urls.py 881 | path('/delete/', v.expense_delete, name='expense_delete'), 882 | ``` 883 | 884 | Escreva o `expense/hx/expense_hx.html` 885 | 886 | ```html 887 | 888 | 889 | ... 890 | 896 | 897 | 898 | 899 | ``` 900 | 901 | --- 902 | 903 | ### client-side-templates 904 | 905 | https://htmx.org/extensions/client-side-templates/ 906 | 907 | Vamos editar: 908 | 909 | 910 | ```python 911 | # expense/models.py 912 | 913 | class Expense(TimeStampedModel): 914 | ... 915 | def to_dict(self): 916 | return { 917 | 'id': self.id, 918 | 'description': self.description, 919 | 'value': self.value, 920 | 'paid': self.paid, 921 | } 922 | ``` 923 | 924 | Escreva o `expense/views.py` 925 | 926 | ```python 927 | # expense/views.py 928 | def expense_json(self): 929 | expenses = Expense.objects.all() 930 | data = [expense.to_dict() for expense in expenses] 931 | return JsonResponse({'data': data}) 932 | 933 | 934 | def expense_client(request): 935 | template_name = 'expense/expense_client.html' 936 | return render(request, template_name) 937 | ``` 938 | 939 | Escreva o `expense/urls.py` 940 | 941 | ```python 942 | # expense/urls.py 943 | ... 944 | path('json/', v.expense_json, name='expense_json'), 945 | path('client/', v.expense_client, name='expense_client'), 946 | ``` 947 | 948 | Escreva 949 | 950 | `touch expense/templates/expense/expense_client.html` 951 | 952 | ```html 953 | 954 | {% extends "base.html" %} 955 | 956 | {% block content %} 957 |

Lista de Despesas (Client side)

958 |

Consumindo API Rest

959 | 960 |
961 | 970 | 971 | 972 | 973 | 974 | 975 | 976 | 977 | 978 | 979 | 980 | 981 | 996 | 997 |
DescriçãoValorPago
998 |
999 | 1000 | {% endblock content %} 1001 | 1002 | {% block js %} 1003 | 1007 | {% endblock js %} 1008 | ``` 1009 | 1010 | Edite `nav.html` 1011 | 1012 | ```html 1013 | Despesas (Client side) 1014 | ``` 1015 | 1016 | **Atenção:** tentar resolver o problema de [cors-headers](https://github.com/adamchainz/django-cors-headers). 1017 | 1018 | 1019 | ### Json Server 1020 | 1021 | #### Instalação 1022 | 1023 | ``` 1024 | npm install -g json-server 1025 | ``` 1026 | 1027 | Crie um `db.json` 1028 | 1029 | ``` 1030 | { 1031 | "expenses": { 1032 | "data": [ 1033 | { "description": "Lanche", "value": 20, "paid": true }, 1034 | { "description": "Conta de luz", "value": 80, "paid": false }, 1035 | { "description": "Refrigerante", "value": 5.5, "paid": true } 1036 | ] 1037 | } 1038 | } 1039 | ``` 1040 | 1041 | #### Server 1042 | 1043 | ``` 1044 | json-server --watch db.json 1045 | ``` 1046 | 1047 | Na pasta principal, escreva 1048 | 1049 | `touch index.html` 1050 | 1051 | Mude o endpoint para `http://localhost:3000/expenses` 1052 | 1053 | ```html 1054 | 1055 | 1056 | 1057 | 1058 | 1059 | 1060 | 1061 | 1062 | htmx 1063 | 1064 | 1065 | 1066 | 1067 | 1068 | 1069 | 1070 | 1071 | 1072 | 1073 | 1074 | 1075 | 1083 | 1084 | 1085 |

Lista de Despesas (Client side)

1086 |

Consumindo API Rest

1087 | 1088 |
1089 | 1090 | 1099 | 1100 | 1101 | 1102 | 1103 | 1104 | 1105 | 1106 | 1107 | 1108 | 1109 | 1110 | 1125 | 1126 |
DescriçãoValorPago
1127 |
1128 | 1129 | 1130 | ``` 1131 | 1132 | --- 1133 | 1134 | ## Bookstore 1135 | 1136 | Veremos um exemplo do uso de Bootstrap modal com htmx. 1137 | 1138 | ``` 1139 | cd backend 1140 | python ../manage.py startapp bookstore 1141 | ``` 1142 | 1143 | Vamos editar: 1144 | 1145 | * nav.html 1146 | * settings.py 1147 | * urls.py 1148 | * bookstore/apps.py 1149 | * bookstore/views.py 1150 | * bookstore/models.py 1151 | * bookstore/admin.py 1152 | * bookstore/urls.py 1153 | * bookstore/templates/bookstore/book_list.html 1154 | * bookstore/templates/bookstore/book_table.html 1155 | * bookstore/templates/bookstore/hx/book_result_hx.html 1156 | 1157 | 1158 | Edite `nav.html` 1159 | 1160 | ```html 1161 | 1164 | ``` 1165 | 1166 | Edite `settings.py` 1167 | 1168 | ```python 1169 | INSTALLED_APPS = [ 1170 | ... 1171 | 'backend.bookstore', 1172 | ... 1173 | ] 1174 | ``` 1175 | 1176 | 1177 | Edite `urls.py` 1178 | 1179 | 1180 | ```python 1181 | from django.contrib import admin 1182 | from django.urls import include, path 1183 | 1184 | urlpatterns = [ 1185 | ... 1186 | path('bookstore/', include('backend.bookstore.urls', namespace='bookstore')), 1187 | ... 1188 | ] 1189 | ``` 1190 | 1191 | 1192 | Edite `bookstore/apps.py` 1193 | 1194 | ```python 1195 | from django.apps import AppConfig 1196 | 1197 | 1198 | class BookstoreConfig(AppConfig): 1199 | default_auto_field = 'django.db.models.BigAutoField' 1200 | name = 'backend.bookstore' 1201 | ``` 1202 | 1203 | Edite `bookstore/views.py` 1204 | 1205 | ```python 1206 | from django.shortcuts import render 1207 | from django.views.decorators.http import require_http_methods 1208 | from django.views.generic import ListView 1209 | 1210 | # from .forms import BookForm 1211 | from .models import Book 1212 | 1213 | 1214 | class BookListView(ListView): 1215 | model = Book 1216 | paginate_by = 10 1217 | ``` 1218 | 1219 | 1220 | Edite `bookstore/models.py` 1221 | 1222 | ```python 1223 | from django.db import models 1224 | from django.urls import reverse_lazy 1225 | 1226 | 1227 | class Book(models.Model): 1228 | title = models.CharField('título', max_length=100, unique=True) 1229 | author = models.CharField('autor', max_length=100, null=True, blank=True) 1230 | 1231 | class Meta: 1232 | ordering = ('title',) 1233 | verbose_name = 'livro' 1234 | verbose_name_plural = 'livros' 1235 | 1236 | def __str__(self): 1237 | return self.title 1238 | 1239 | def get_absolute_url(self): 1240 | return reverse_lazy('bookstore:book_detail', kwargs={'pk': self.pk}) 1241 | ``` 1242 | 1243 | 1244 | Edite `bookstore/admin.py` 1245 | 1246 | ```python 1247 | from django.contrib import admin 1248 | 1249 | from .models import Book 1250 | 1251 | 1252 | @admin.register(Book) 1253 | class BookAdmin(admin.ModelAdmin): 1254 | list_display = ('title', 'author') 1255 | search_fields = ('title', 'author') 1256 | ``` 1257 | 1258 | 1259 | Edite `bookstore/urls.py` 1260 | 1261 | ```python 1262 | from django.urls import path 1263 | 1264 | from backend.bookstore import views as v 1265 | 1266 | app_name = 'bookstore' 1267 | 1268 | 1269 | urlpatterns = [ 1270 | path('', v.BookListView.as_view(), name='book_list'), 1271 | ] 1272 | ``` 1273 | 1274 | 1275 | ``` 1276 | mkdir -p backend/bookstore/templates/bookstore/includes 1277 | ``` 1278 | 1279 | ```html 1280 | cat << EOF > backend/bookstore/templates/bookstore/book_list.html 1281 | 1282 | {% extends "base.html" %} 1283 | 1284 | {% block content %} 1285 |
1286 |
1287 |
1288 |

Lista de Livros

1289 |
1290 |
1291 | Adicionar 1292 |
1293 |
1294 | 1295 | 1296 | 1297 | 1298 | 1299 | 1300 | 1301 | 1302 | 1303 | {% include "./book_table.html" %} 1304 | 1305 |
TítuloAutorAções
1306 |
1307 | {% endblock content %} 1308 | EOF 1309 | ``` 1310 | 1311 | ```html 1312 | cat << EOF > backend/bookstore/templates/bookstore/book_table.html 1313 | 1314 | {% for object in object_list %} 1315 | {% include "./hx/book_result_hx.html" %} 1316 | {% endfor %} 1317 | EOF 1318 | ``` 1319 | 1320 | ```html 1321 | cat << EOF > backend/bookstore/templates/bookstore/hx/book_result_hx.html 1322 | 1323 | 1324 | 1325 | {{ object.title }} 1326 | 1327 | {{ object.author|default:'---' }} 1328 | 1329 | 1330 | EOF 1331 | ``` 1332 | 1333 | ## Adicionar 1334 | 1335 | Vamos editar: 1336 | 1337 | * bookstore/views.py 1338 | * bookstore/urls.py 1339 | * bookstore/forms.py 1340 | * bookstore/templates/bookstore/book_list.html 1341 | * bookstore/templates/bookstore/includes/add_modal.html 1342 | 1343 | 1344 | Edite `bookstore/views.py` 1345 | 1346 | 1347 | ```python 1348 | ... 1349 | from .forms import BookForm 1350 | from .models import Book 1351 | 1352 | 1353 | def book_create(request): 1354 | template_name = 'bookstore/hx/book_form_hx.html' 1355 | form = BookForm(request.POST or None) 1356 | 1357 | if request.method == 'POST': 1358 | if form.is_valid(): 1359 | book = form.save() 1360 | template_name = 'bookstore/hx/book_result_hx.html' 1361 | context = {'object': book} 1362 | return render(request, template_name, context) 1363 | 1364 | context = {'form': form} 1365 | return render(request, template_name, context) 1366 | 1367 | ``` 1368 | 1369 | 1370 | Edite `bookstore/urls.py` 1371 | 1372 | ```python 1373 | ... 1374 | path('create/', v.book_create, name='book_create'), 1375 | ... 1376 | ``` 1377 | 1378 | ```python 1379 | cat << EOF > backend/bookstore/forms.py 1380 | from django import forms 1381 | 1382 | from .models import Book 1383 | 1384 | 1385 | class BookForm(forms.ModelForm): 1386 | required_css_class = 'required' 1387 | 1388 | class Meta: 1389 | model = Book 1390 | fields = '__all__' 1391 | 1392 | def __init__(self, *args, **kwargs): 1393 | super(BookForm, self).__init__(*args, **kwargs) 1394 | for field_name, field in self.fields.items(): 1395 | field.widget.attrs['class'] = 'form-control' 1396 | EOF 1397 | ``` 1398 | 1399 | Edite `backend/bookstore/templates/bookstore/book_list.html` 1400 | 1401 | ```html 1402 | 1403 | 1404 | Adicionar 1413 | ... 1414 | 1415 | {% include "./includes/add_modal.html" %} 1416 | 1417 | {% endblock content %} 1418 | 1419 | ``` 1420 | 1421 | 1422 | ```html 1423 | cat << EOF > backend/bookstore/templates/bookstore/includes/add_modal.html 1424 | 1425 | 1443 | EOF 1444 | 1445 | ``` 1446 | 1447 | ```html 1448 | cat << EOF > backend/bookstore/templates/bookstore/hx/book_form_hx.html 1449 | 1450 | 1454 |
1460 | 1473 | 1477 |
1478 | 1479 | 1484 | EOF 1485 | ``` 1486 | 1487 | ## Detalhes 1488 | 1489 | Vamos editar: 1490 | 1491 | * bookstore/views.py 1492 | * bookstore/urls.py 1493 | * bookstore/templates/bookstore/book_detail.html 1494 | * bookstore/templates/bookstore/includes/detail_modal.html 1495 | * bookstore/templates/bookstore/hx/book_result_hx.html 1496 | * bookstore/templates/bookstore/book_list.html 1497 | 1498 | 1499 | Edite `bookstore/views.py` 1500 | 1501 | ```python 1502 | def book_detail(request, pk): 1503 | template_name = 'bookstore/book_detail.html' 1504 | obj = Book.objects.get(pk=pk) 1505 | context = {'object': obj} 1506 | return render(request, template_name, context) 1507 | ``` 1508 | 1509 | Edite `bookstore/urls.py` 1510 | 1511 | ```python 1512 | ... 1513 | path('/', v.book_detail, name='book_detail'), 1514 | ... 1515 | ``` 1516 | 1517 | ```html 1518 | cat << EOF > backend/bookstore/templates/bookstore/book_detail.html 1519 | 1523 | 1531 | 1534 | EOF 1535 | ``` 1536 | 1537 | 1538 | ```html 1539 | cat << EOF > backend/bookstore/templates/bookstore/includes/detail_modal.html 1540 | 1541 | 1559 | EOF 1560 | ``` 1561 | 1562 | 1563 | 1564 | Edite `backend/bookstore/templates/bookstore/hx/book_result_hx.html` 1565 | 1566 | ```html 1567 | 1568 | 1569 | 1570 | {{ object.title }} 1579 | 1580 | {{ object.author|default:'---' }} 1581 | 1582 | 1583 | ``` 1584 | 1585 | Edite `backend/bookstore/templates/bookstore/book_list.html` 1586 | 1587 | ```html 1588 | ... 1589 | {% include "./includes/detail_modal.html" %} 1590 | ... 1591 | ``` 1592 | 1593 | 1594 | ## Editar 1595 | 1596 | Vamos editar: 1597 | 1598 | * bookstore/views.py 1599 | * bookstore/urls.py 1600 | * bookstore/templates/bookstore/book_list.html 1601 | * bookstore/templates/bookstore/includes/update_modal.html 1602 | * bookstore/templates/bookstore/hx/book_result_hx.html 1603 | * bookstore/templates/bookstore/book_update_form.html 1604 | 1605 | 1606 | 1607 | Edite `bookstore/views.py` 1608 | 1609 | ```python 1610 | def book_update(request, pk): 1611 | template_name = 'bookstore/book_update_form.html' 1612 | instance = Book.objects.get(pk=pk) 1613 | form = BookForm(request.POST or None, instance=instance) 1614 | 1615 | if request.method == 'POST': 1616 | if form.is_valid(): 1617 | book = form.save() 1618 | template_name = 'bookstore/hx/book_result_hx.html' 1619 | context = {'object': book} 1620 | return render(request, template_name, context) 1621 | 1622 | context = {'form': form, 'object': instance} 1623 | return render(request, template_name, context) 1624 | 1625 | ``` 1626 | 1627 | Edite `bookstore/urls.py` 1628 | 1629 | ```python 1630 | ... 1631 | path('/update/', v.book_update, name='book_update'), 1632 | ... 1633 | ``` 1634 | 1635 | 1636 | Edite `backend/bookstore/templates/bookstore/book_list.html` 1637 | 1638 | ```html 1639 | {% include "./includes/update_modal.html" %} 1640 | ``` 1641 | 1642 | ```html 1643 | cat << EOF > backend/bookstore/templates/bookstore/includes/update_modal.html 1644 | 1645 | 1663 | EOF 1664 | ``` 1665 | 1666 | Editar `backend/bookstore/templates/bookstore/hx/book_result_hx.html` 1667 | 1668 | ```html 1669 | 1676 | 1677 | 1678 | ``` 1679 | 1680 | ```html 1681 | cat << EOF > backend/bookstore/templates/bookstore/book_update_form.html 1682 | 1686 |
1692 | 1705 | 1709 |
1710 | 1711 | 1716 | EOF 1717 | ``` 1718 | 1719 | ## Deletar 1720 | 1721 | Vamos editar: 1722 | 1723 | * bookstore/views.py 1724 | * bookstore/urls.py 1725 | * bookstore/templates/bookstore/book_list.html 1726 | * bookstore/templates/bookstore/hx/book_result_hx.html 1727 | 1728 | 1729 | 1730 | Edite `bookstore/views.py` 1731 | 1732 | ```python 1733 | @require_http_methods(['DELETE']) 1734 | def book_delete(request, pk): 1735 | template_name = 'bookstore/book_table.html' 1736 | obj = Book.objects.get(pk=pk) 1737 | obj.delete() 1738 | return render(request, template_name) 1739 | 1740 | ``` 1741 | 1742 | 1743 | Edite `bookstore/urls.py` 1744 | 1745 | ```python 1746 | ... 1747 | path('/delete/', v.book_delete, name='book_delete'), 1748 | ... 1749 | ``` 1750 | 1751 | 1752 | Editar `backend/bookstore/templates/bookstore/book_list.html` 1753 | 1754 | ```html 1755 | 1756 | {% block js %} 1757 | 1763 | {% endblock js %} 1764 | 1765 | ``` 1766 | 1767 | 1768 | Editar `backend/bookstore/templates/bookstore/hx/book_result_hx.html` 1769 | 1770 | Ao lado icone de editar. 1771 | 1772 | ```html 1773 | ... 1774 | 1780 | 1781 | 1782 | ... 1783 | ``` 1784 | 1785 | ## Like e Unlike 1786 | 1787 | Vamos editar: 1788 | 1789 | * bookstore/models.py 1790 | * bookstore/admin.py 1791 | * bookstore/forms.py 1792 | * bookstore/book_list.html 1793 | * bookstore/hx/book_result_hx.html 1794 | * bookstore/urls.py 1795 | * bookstore/views.py 1796 | 1797 | Edite `models.py` 1798 | 1799 | ```python 1800 | ... 1801 | like = models.BooleanField(null=True) 1802 | ``` 1803 | 1804 | 1805 | Edite `admin.py` 1806 | 1807 | ```python 1808 | ... 1809 | list_display = ('title', 'author', 'like') 1810 | ... 1811 | ``` 1812 | 1813 | 1814 | Edite `forms.py` 1815 | 1816 | ```python 1817 | ... 1818 | fields = ('title', 'author') 1819 | ... 1820 | ``` 1821 | 1822 | 1823 | Edite `book_list.html` 1824 | 1825 | ```html 1826 | Gostou? 1827 | ``` 1828 | 1829 | 1830 | Edite `hx/book_result_hx.html` 1831 | 1832 | ```html 1833 | 1834 | 1839 | {% if object.like %} 1840 | 1841 | {% else %} 1842 | 1843 | {% endif %} 1844 | 1845 | 1851 | {% if not object.like %} 1852 | 1853 | {% else %} 1854 | 1855 | {% endif %} 1856 | 1857 | 1858 | ``` 1859 | 1860 | 1861 | Edite `urls.py` 1862 | 1863 | ```python 1864 | ... 1865 | path('/like/', v.book_like, name='book_like'), 1866 | path('/unlike/', v.book_unlike, name='book_unlike'), 1867 | ``` 1868 | 1869 | 1870 | Edite `views.py` 1871 | 1872 | ```python 1873 | @require_http_methods(['POST']) 1874 | def book_like(request, pk): 1875 | template_name = 'bookstore/hx/book_result_hx.html' 1876 | book = Book.objects.get(pk=pk) 1877 | book.like = True 1878 | book.save() 1879 | context = {'object': book} 1880 | return render(request, template_name, context) 1881 | 1882 | 1883 | @require_http_methods(['POST']) 1884 | def book_unlike(request, pk): 1885 | template_name = 'bookstore/hx/book_result_hx.html' 1886 | book = Book.objects.get(pk=pk) 1887 | book.like = False 1888 | book.save() 1889 | context = {'object': book} 1890 | return render(request, template_name, context) 1891 | ``` 1892 | 1893 | 1894 | ## Criar uma nova categoria 1895 | 1896 | Vamos editar: 1897 | 1898 | * settings.py 1899 | * urls.py 1900 | * apps.py 1901 | * models.py 1902 | * admin.py 1903 | * urls.py 1904 | * views.py 1905 | * nav.html 1906 | * product_list.html 1907 | * product_table.html 1908 | * includes/add_modal.html 1909 | * hx/product_result_hx.html 1910 | * hx/category_modal_form_hx.html 1911 | 1912 | 1913 | Edite `settings.py` 1914 | 1915 | ```python 1916 | 'backend.product', 1917 | ``` 1918 | 1919 | 1920 | Edite `urls.py` 1921 | 1922 | ```python 1923 | path('product/', include('backend.product.urls', namespace='product')), 1924 | ``` 1925 | 1926 | 1927 | Edite `apps.py` 1928 | 1929 | ```python 1930 | name = 'backend.product' 1931 | ``` 1932 | 1933 | 1934 | Edite `models.py` 1935 | 1936 | ```python 1937 | from django.db import models 1938 | 1939 | 1940 | class Category(models.Model): 1941 | title = models.CharField('título', max_length=100, unique=True) 1942 | 1943 | class Meta: 1944 | ordering = ('title',) 1945 | verbose_name = 'categoria' 1946 | verbose_name_plural = 'categorias' 1947 | 1948 | def __str__(self): 1949 | return self.title 1950 | 1951 | 1952 | class Product(models.Model): 1953 | title = models.CharField('título', max_length=100, unique=True) 1954 | category = models.ForeignKey( 1955 | Category, 1956 | on_delete=models.SET_NULL, 1957 | verbose_name='categoria', 1958 | related_name='products', 1959 | null=True, 1960 | blank=True 1961 | ) 1962 | 1963 | class Meta: 1964 | ordering = ('title',) 1965 | verbose_name = 'produto' 1966 | verbose_name_plural = 'produtos' 1967 | 1968 | def __str__(self): 1969 | return self.title 1970 | 1971 | ``` 1972 | 1973 | 1974 | Edite `admin.py` 1975 | 1976 | ```python 1977 | from django.contrib import admin 1978 | 1979 | from .models import Category, Product 1980 | 1981 | 1982 | @admin.register(Category) 1983 | class CategoryAdmin(admin.ModelAdmin): 1984 | list_display = ('__str__',) 1985 | search_fields = ('title',) 1986 | 1987 | 1988 | @admin.register(Product) 1989 | class ProductAdmin(admin.ModelAdmin): 1990 | list_display = ('__str__', 'category') 1991 | search_fields = ('title', 'category__title') 1992 | 1993 | ``` 1994 | 1995 | 1996 | Edite `urls.py` 1997 | 1998 | ```python 1999 | from django.urls import path 2000 | 2001 | from backend.product import views as v 2002 | 2003 | app_name = 'product' 2004 | 2005 | 2006 | urlpatterns = [ 2007 | path('', v.product_list, name='product_list'), 2008 | path('category//create/', v.category_create, name='category_create'), 2009 | ] 2010 | 2011 | ``` 2012 | 2013 | 2014 | Edite `views.py` 2015 | 2016 | ```python 2017 | from django.shortcuts import render 2018 | 2019 | from .models import Category, Product 2020 | 2021 | 2022 | def product_list(request): 2023 | template_name = 'product/product_list.html' 2024 | object_list = Product.objects.all() 2025 | categories = Category.objects.all() 2026 | 2027 | context = { 2028 | 'object_list': object_list, 2029 | 'categories': categories, 2030 | } 2031 | return render(request, template_name, context) 2032 | 2033 | 2034 | def category_create(request, pk): 2035 | template_name = 'product/hx/category_modal_form_hx.html' 2036 | product = Product.objects.get(pk=pk) 2037 | 2038 | if request.method == 'POST': 2039 | title = request.POST.get('categoria') 2040 | # Cria a nova categoria 2041 | category = Category.objects.create(title=title) 2042 | 2043 | # Associa a nova categoria ao produto atual. 2044 | product.category = category 2045 | product.save() 2046 | 2047 | template_name = 'product/hx/product_result_hx.html' 2048 | 2049 | categories = Category.objects.all() 2050 | context = { 2051 | 'object': product, 2052 | 'categories': categories, 2053 | } 2054 | return render(request, template_name, context) 2055 | 2056 | context = {'object': product} 2057 | return render(request, template_name, context) 2058 | 2059 | ``` 2060 | 2061 | 2062 | Edite `nav.html` 2063 | 2064 | ```html 2065 | 2068 | 2069 | ``` 2070 | 2071 | 2072 | Edite `product_list.html` 2073 | 2074 | ```html 2075 | 2076 | {% extends "base.html" %} 2077 | 2078 | {% block content %} 2079 |
2080 |
2081 |
2082 |

Lista de Produtos

2083 |
2084 |
2085 | 2086 | 2087 | 2088 | 2089 | 2090 | 2091 | 2092 | 2093 | {% include "./product_table.html" %} 2094 | 2095 |
ProdutoCategoria
2096 |
2097 | 2098 | {% include "./includes/add_modal.html" %} 2099 | 2100 | {% endblock content %} 2101 | 2102 | ``` 2103 | 2104 | 2105 | Edite `product_table.html` 2106 | 2107 | ```html 2108 | 2109 | {% for object in object_list %} 2110 | {% include "./hx/product_result_hx.html" %} 2111 | {% endfor %} 2112 | 2113 | ``` 2114 | 2115 | 2116 | Edite `hx/product_result_hx.html` 2117 | 2118 | ```html 2119 | 2120 | 2121 | {{ object.title }} 2122 | 2123 |
2124 |
2125 | 2138 |
2139 |
2140 | 2147 | 2148 | 2149 |
2150 |
2151 | 2152 | 2153 | ``` 2154 | 2155 | 2156 | Edite `includes/add_modal.html` 2157 | 2158 | ```html 2159 | 2160 | 2167 | 2168 | ``` 2169 | 2170 | 2171 | Edite `hx/category_modal_form_hx.html` 2172 | 2173 | ```html 2174 | 2175 | 2180 |
2185 | {% csrf_token %} 2186 | 2190 | 2194 |
2195 | 2196 | 2201 | 2202 | ``` 2203 | 2204 | 2205 | `./manage.py shell_plus` 2206 | 2207 | 2208 | ```python 2209 | Category.objects.create(title='bebida') 2210 | Category.objects.create(title='lanche') 2211 | 2212 | produtos = [ 2213 | ('Água mineral', 'bebida'), 2214 | ('Refrigerante 350ml', 'bebida'), 2215 | ('Batata frita', 'bebida'), 2216 | ('Casquinha', ''), 2217 | ] 2218 | 2219 | for produto in produtos: 2220 | categoria_titulo = produto[1] 2221 | category = Category.objects.filter(title=categoria_titulo).first() 2222 | if category: 2223 | Product.objects.create(title=produto[0], category=category) 2224 | else: 2225 | Product.objects.create(title=produto[0]) 2226 | 2227 | ``` 2228 | 2229 | ## Trocar a categoria 2230 | 2231 | Vamos editar: 2232 | 2233 | * hx/product_result_hx.html 2234 | * product_list.html 2235 | * urls.py 2236 | * views.py 2237 | 2238 | 2239 | Edite `hx/product_result_hx.html` 2240 | 2241 | ```html 2242 | 13 | 14 | {{ object.description }} 15 | {{ object.value }} 16 | 17 | {% if object.paid %} 18 | 19 | 20 | 21 | {% else %} 22 | 23 | 24 | 25 | {% endif %} 26 | 27 | 28 | 29 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /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.expense_list, name='expense_list'), 10 | path('json/', v.expense_json, name='expense_json'), 11 | path('client/', v.expense_client, name='expense_client'), 12 | path('create/', v.expense_create, name='expense_create'), 13 | path('/delete/', v.expense_delete, name='expense_delete'), 14 | path('expense/paid/', v.expense_paid, name='expense_paid'), 15 | path('expense/no-paid/', v.expense_no_paid, name='expense_no_paid'), 16 | path('/', v.expense_detail, name='expense_detail'), 17 | path('/update/', v.expense_update, name='expense_update'), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/expense/views.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse 2 | from django.shortcuts import render 3 | from django.views.decorators.http import require_http_methods 4 | 5 | from .forms import ExpenseForm 6 | from .models import Expense 7 | 8 | 9 | def expense_list(request): 10 | template_name = 'expense/expense_list.html' 11 | form = ExpenseForm(request.POST or None) 12 | 13 | expenses = Expense.objects.all() 14 | 15 | context = {'object_list': expenses, 'form': form} 16 | return render(request, template_name, context) 17 | 18 | 19 | @require_http_methods(['POST']) 20 | def expense_create(request): 21 | form = ExpenseForm(request.POST or None) 22 | 23 | if form.is_valid(): 24 | expense = form.save() 25 | 26 | context = {'object': expense} 27 | return render(request, 'expense/hx/expense_hx.html', context) 28 | 29 | 30 | @require_http_methods(['DELETE']) 31 | def expense_delete(request, pk): 32 | obj = Expense.objects.get(pk=pk) 33 | obj.delete() 34 | return render(request, 'expense/expense_table.html') 35 | 36 | 37 | @require_http_methods(['POST']) 38 | def expense_paid(request): 39 | ids = request.POST.getlist('ids') 40 | 41 | # Edita as despesas selecionadas. 42 | Expense.objects.filter(id__in=ids).update(paid=True) 43 | 44 | # Retorna todas as despesas novamente. 45 | expenses = Expense.objects.all() 46 | 47 | context = {'object_list': expenses} 48 | return render(request, 'expense/expense_table.html', context) 49 | 50 | 51 | @require_http_methods(['POST']) 52 | def expense_no_paid(request): 53 | ids = request.POST.getlist('ids') 54 | 55 | # Edita as despesas selecionadas. 56 | Expense.objects.filter(id__in=ids).update(paid=False) 57 | 58 | # Retorna todas as despesas novamente. 59 | expenses = Expense.objects.all() 60 | 61 | context = {'object_list': expenses} 62 | return render(request, 'expense/expense_table.html', context) 63 | 64 | 65 | def expense_detail(request, pk): 66 | obj = Expense.objects.get(pk=pk) 67 | form = ExpenseForm(request.POST or None, instance=obj) 68 | 69 | context = {'object': obj, 'form': form} 70 | return render(request, 'expense/hx/expense_detail_hx.html', context) 71 | 72 | 73 | def expense_update(request, pk): 74 | obj = Expense.objects.get(pk=pk) 75 | form = ExpenseForm(request.POST or None, instance=obj) 76 | context = {'object': obj} 77 | 78 | if request.method == 'POST': 79 | if form.is_valid(): 80 | form.save() 81 | 82 | return render(request, 'expense/hx/expense_hx.html', context) 83 | 84 | 85 | def expense_json(self): 86 | expenses = Expense.objects.all() 87 | data = [expense.to_dict() for expense in expenses] 88 | return JsonResponse({'data': data}) 89 | 90 | 91 | def expense_client(request): 92 | template_name = 'expense/expense_client.html' 93 | return render(request, template_name) 94 | -------------------------------------------------------------------------------- /backend/product/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-htmx-tutorial/903c6e4c501a0a696e9b12f46cc99eb9ae770b31/backend/product/__init__.py -------------------------------------------------------------------------------- /backend/product/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Category, Product 4 | 5 | 6 | @admin.register(Category) 7 | class CategoryAdmin(admin.ModelAdmin): 8 | list_display = ('__str__',) 9 | search_fields = ('title',) 10 | 11 | 12 | @admin.register(Product) 13 | class ProductAdmin(admin.ModelAdmin): 14 | list_display = ('__str__', 'category') 15 | search_fields = ('title', 'category__title') 16 | -------------------------------------------------------------------------------- /backend/product/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ProductConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'backend.product' 7 | -------------------------------------------------------------------------------- /backend/product/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.5 on 2021-09-08 01:40 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Category', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('title', models.CharField(max_length=100, unique=True, verbose_name='título')), 20 | ], 21 | options={ 22 | 'verbose_name': 'categoria', 23 | 'verbose_name_plural': 'categorias', 24 | 'ordering': ('title',), 25 | }, 26 | ), 27 | migrations.CreateModel( 28 | name='Product', 29 | fields=[ 30 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 31 | ('title', models.CharField(max_length=100, unique=True, verbose_name='título')), 32 | ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='products', to='product.category', verbose_name='categoria')), 33 | ], 34 | options={ 35 | 'verbose_name': 'produto', 36 | 'verbose_name_plural': 'produtos', 37 | 'ordering': ('title',), 38 | }, 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /backend/product/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-htmx-tutorial/903c6e4c501a0a696e9b12f46cc99eb9ae770b31/backend/product/migrations/__init__.py -------------------------------------------------------------------------------- /backend/product/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Category(models.Model): 5 | title = models.CharField('título', max_length=100, unique=True) 6 | 7 | class Meta: 8 | ordering = ('title',) 9 | verbose_name = 'categoria' 10 | verbose_name_plural = 'categorias' 11 | 12 | def __str__(self): 13 | return self.title 14 | 15 | 16 | class Product(models.Model): 17 | title = models.CharField('título', max_length=100, unique=True) 18 | category = models.ForeignKey( 19 | Category, 20 | on_delete=models.SET_NULL, 21 | verbose_name='categoria', 22 | related_name='products', 23 | null=True, 24 | blank=True 25 | ) 26 | 27 | class Meta: 28 | ordering = ('title',) 29 | verbose_name = 'produto' 30 | verbose_name_plural = 'produtos' 31 | 32 | def __str__(self): 33 | return self.title 34 | -------------------------------------------------------------------------------- /backend/product/templates/product/hx/category_modal_form_hx.html: -------------------------------------------------------------------------------- 1 | 2 | 7 |
12 | {% csrf_token %} 13 | 17 | 21 |
22 | 23 | 28 | -------------------------------------------------------------------------------- /backend/product/templates/product/hx/product_result_hx.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ object.title }} 4 | 5 |
6 |
7 | 22 |
23 |
24 | 31 | 32 | 33 |
34 |
35 | 36 | -------------------------------------------------------------------------------- /backend/product/templates/product/includes/add_modal.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | -------------------------------------------------------------------------------- /backend/product/templates/product/product_list.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |

Lista de Produtos

9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% include "./product_table.html" %} 20 | 21 |
ProdutoCategoria
22 |
23 | 24 | {% include "./includes/add_modal.html" %} 25 | 26 | {% endblock content %} 27 | 28 | {% block js %} 29 | 35 | {% endblock js %} 36 | -------------------------------------------------------------------------------- /backend/product/templates/product/product_table.html: -------------------------------------------------------------------------------- 1 | 2 | {% for object in object_list %} 3 | {% include "./hx/product_result_hx.html" %} 4 | {% endfor %} 5 | -------------------------------------------------------------------------------- /backend/product/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/product/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from backend.product import views as v 4 | 5 | app_name = 'product' 6 | 7 | 8 | urlpatterns = [ 9 | path('', v.product_list, name='product_list'), 10 | path('category//create/', v.category_create, name='category_create'), 11 | path('/category/update/', v.category_update, name='category_update'), # noqa E501 12 | ] 13 | -------------------------------------------------------------------------------- /backend/product/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.shortcuts import render 3 | from django.views.decorators.http import require_http_methods 4 | 5 | from .models import Category, Product 6 | 7 | 8 | def product_list(request): 9 | template_name = 'product/product_list.html' 10 | object_list = Product.objects.all() 11 | categories = Category.objects.all() 12 | 13 | context = { 14 | 'object_list': object_list, 15 | 'categories': categories, 16 | } 17 | return render(request, template_name, context) 18 | 19 | 20 | def category_create(request, pk): 21 | template_name = 'product/hx/category_modal_form_hx.html' 22 | product = Product.objects.get(pk=pk) 23 | 24 | if request.method == 'POST': 25 | title = request.POST.get('categoria') 26 | # Cria a nova categoria 27 | category = Category.objects.create(title=title) 28 | 29 | # Associa a nova categoria ao produto atual. 30 | product.category = category 31 | product.save() 32 | 33 | template_name = 'product/hx/product_result_hx.html' 34 | 35 | categories = Category.objects.all() 36 | context = { 37 | 'object': product, 38 | 'categories': categories, 39 | } 40 | return render(request, template_name, context) 41 | 42 | context = {'object': product} 43 | return render(request, template_name, context) 44 | 45 | 46 | @require_http_methods(['POST']) 47 | def category_update(request, product_pk): 48 | product = Product.objects.get(pk=product_pk) 49 | 50 | category_pk = request.POST.get('category') 51 | category = Category.objects.get(pk=category_pk) 52 | 53 | product.category = category 54 | product.save() 55 | return HttpResponse('ok') 56 | -------------------------------------------------------------------------------- /backend/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for backend project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/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/3.2/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 | # thirty apps 44 | 'django_extensions', 45 | 'corsheaders', 46 | # my apps 47 | 'backend.core', 48 | 'backend.bookstore', 49 | 'backend.expense', 50 | 'backend.state', 51 | 'backend.product', 52 | ] 53 | 54 | MIDDLEWARE = [ 55 | 'django.middleware.security.SecurityMiddleware', 56 | 'django.contrib.sessions.middleware.SessionMiddleware', 57 | 'corsheaders.middleware.CorsMiddleware', 58 | 'django.middleware.common.CommonMiddleware', 59 | 'django.middleware.csrf.CsrfViewMiddleware', 60 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 61 | 'django.contrib.messages.middleware.MessageMiddleware', 62 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 63 | ] 64 | 65 | 66 | # CORS_ALLOW_ALL_ORIGINS = True 67 | 68 | CORS_ORIGIN_ALLOW_ALL = True 69 | 70 | CORS_ALLOW_CREDENTIALS = True 71 | 72 | # CORS_ALLOWED_ORIGIN_REGEXES = [ 73 | # 'http://127.0.0.1:8001', 74 | # 'http://127.0.0.1:5000', 75 | # ] 76 | 77 | ROOT_URLCONF = 'backend.urls' 78 | 79 | TEMPLATES = [ 80 | { 81 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 82 | 'DIRS': [], 83 | 'APP_DIRS': True, 84 | 'OPTIONS': { 85 | 'context_processors': [ 86 | 'django.template.context_processors.debug', 87 | 'django.template.context_processors.request', 88 | 'django.contrib.auth.context_processors.auth', 89 | 'django.contrib.messages.context_processors.messages', 90 | ], 91 | }, 92 | }, 93 | ] 94 | 95 | WSGI_APPLICATION = 'backend.wsgi.application' 96 | 97 | 98 | # Database 99 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 100 | 101 | default_dburl = 'sqlite:///' + str(BASE_DIR / 'db.sqlite3') 102 | DATABASES = { 103 | 'default': config('DATABASE_URL', default=default_dburl, cast=dburl), 104 | } 105 | 106 | 107 | # Password validation 108 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 109 | 110 | AUTH_PASSWORD_VALIDATORS = [ 111 | { 112 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 113 | }, 114 | { 115 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 116 | }, 117 | { 118 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 119 | }, 120 | { 121 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 122 | }, 123 | ] 124 | 125 | 126 | # Internationalization 127 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 128 | 129 | LANGUAGE_CODE = 'pt-br' # 'en-us' 130 | 131 | TIME_ZONE = 'America/Sao_Paulo' # 'UTC' 132 | 133 | USE_I18N = True 134 | 135 | USE_L10N = True 136 | 137 | USE_TZ = True 138 | 139 | USE_THOUSAND_SEPARATOR = True 140 | 141 | DECIMAL_SEPARATOR = ',' 142 | 143 | 144 | # Static files (CSS, JavaScript, Images) 145 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 146 | 147 | STATIC_URL = '/static/' 148 | STATIC_ROOT = BASE_DIR.joinpath('staticfiles') 149 | 150 | # Default primary key field type 151 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 152 | 153 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 154 | -------------------------------------------------------------------------------- /backend/state/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-htmx-tutorial/903c6e4c501a0a696e9b12f46cc99eb9ae770b31/backend/state/__init__.py -------------------------------------------------------------------------------- /backend/state/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class StateConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'backend.state' 7 | -------------------------------------------------------------------------------- /backend/state/states.py: -------------------------------------------------------------------------------- 1 | states = { 2 | "Norte": { 3 | "AC": "Acre", 4 | "AM": "Amazonas", 5 | "AP": "Amapá", 6 | "PA": "Pará", 7 | "RO": "Rondônia", 8 | "RR": "Roraima", 9 | "TO": "Tocantins" 10 | }, 11 | "Nordeste": { 12 | "AL": "Alagoas", 13 | "BA": "Bahia", 14 | "CE": "Ceará", 15 | "MA": "Maranhão", 16 | "PB": "Paraíba", 17 | "PE": "Pernambuco", 18 | "PI": "Piauí", 19 | "RN": "Rio Grande do Norte", 20 | "SE": "Sergipe" 21 | }, 22 | "Sul": { 23 | "PR": "Paraná", 24 | "RS": "Rio Grande do Sul", 25 | "SC": "Santa Catarina" 26 | }, 27 | "Sudeste": { 28 | "ES": "Espírito Santo", 29 | "MG": "Minas Gerais", 30 | "RJ": "Rio de Janeiro", 31 | "SP": "São Paulo" 32 | }, 33 | "Centro-Oeste": { 34 | "GO": "Goiás", 35 | "MS": "Mato Grosso do Sul", 36 | "MT": "Mato Grosso" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/state/templates/state/hx/state_hx.html: -------------------------------------------------------------------------------- 1 | {% for uf in ufs %} 2 | 3 | {{ uf.1 }} 4 | 5 | {% endfor %} -------------------------------------------------------------------------------- /backend/state/templates/state/hx/uf_hx.html: -------------------------------------------------------------------------------- 1 | {% for uf in ufs %} 2 | 3 | {% endfor %} -------------------------------------------------------------------------------- /backend/state/templates/state/state_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

Regiões e Estados do Brasil

5 |

Filtro com dropdowns dependentes

6 | 7 |
8 | 9 |
10 | 11 | 22 |
23 | 24 |
25 | 26 | 33 |
34 | 35 |
36 | 37 |
38 | 39 |

Filtrando várias tabelas com um clique

40 | 41 |
42 | 43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {% for region in regions %} 52 | 56 | 59 | 60 | {% endfor %} 61 | 62 |
Região
57 | {{ region.1 }} 58 |
63 |
64 | 65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
Estados
76 |
77 | 78 |
79 | {% endblock content %} 80 | -------------------------------------------------------------------------------- /backend/state/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from backend.state import views as v 4 | 5 | app_name = 'state' 6 | 7 | 8 | urlpatterns = [ 9 | path('', v.state_list, name='state_list'), 10 | path('uf/', v.uf_list, name='uf_list'), 11 | path('result/', v.state_result, name='state_result'), 12 | ] 13 | -------------------------------------------------------------------------------- /backend/state/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | from .states import states 4 | 5 | 6 | def state_list(request): 7 | template_name = 'state/state_list.html' 8 | 9 | regions = ( 10 | ('n', 'Norte'), 11 | ('ne', 'Nordeste'), 12 | ('s', 'Sul'), 13 | ('se', 'Sudeste'), 14 | ('co', 'Centro-Oeste'), 15 | ) 16 | 17 | context = {'regions': regions} 18 | return render(request, template_name, context) 19 | 20 | 21 | def get_states(region): 22 | return [state for state in states.get(region).items()] 23 | 24 | 25 | def uf_list(request): 26 | template_name = 'state/hx/uf_hx.html' 27 | region = request.GET.get('region') 28 | 29 | ufs = { 30 | 'n': get_states('Norte'), 31 | 'ne': get_states('Nordeste'), 32 | 's': get_states('Sul'), 33 | 'se': get_states('Sudeste'), 34 | 'co': get_states('Centro-Oeste'), 35 | } 36 | 37 | context = {'ufs': ufs[region]} 38 | return render(request, template_name, context) 39 | 40 | 41 | def state_result(request): 42 | template_name = 'state/hx/state_hx.html' 43 | region = request.GET.get('region') 44 | 45 | ufs = { 46 | 'n': get_states('Norte'), 47 | 'ne': get_states('Nordeste'), 48 | 's': get_states('Sul'), 49 | 'se': get_states('Sudeste'), 50 | 'co': get_states('Centro-Oeste'), 51 | } 52 | 53 | context = {'ufs': ufs[region]} 54 | return render(request, template_name, context) 55 | -------------------------------------------------------------------------------- /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('bookstore/', include('backend.bookstore.urls', namespace='bookstore')), 8 | path('state/', include('backend.state.urls', namespace='state')), 9 | path('product/', include('backend.product.urls', namespace='product')), 10 | path('admin/', admin.site.urls), 11 | ] 12 | -------------------------------------------------------------------------------- /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/3.2/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 | -------------------------------------------------------------------------------- /db.json: -------------------------------------------------------------------------------- 1 | { 2 | "expenses": { 3 | "data": [ 4 | { "description": "Lanche", "value": 20, "paid": true }, 5 | { "description": "Conta de luz", "value": 80, "paid": false }, 6 | { "description": "Refrigerante", "value": 5.5, "paid": true } 7 | ] 8 | } 9 | } -------------------------------------------------------------------------------- /img/01_expense_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-htmx-tutorial/903c6e4c501a0a696e9b12f46cc99eb9ae770b31/img/01_expense_add.png -------------------------------------------------------------------------------- /img/02_expense_bulk_update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-htmx-tutorial/903c6e4c501a0a696e9b12f46cc99eb9ae770b31/img/02_expense_bulk_update.png -------------------------------------------------------------------------------- /img/03_expense_update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-htmx-tutorial/903c6e4c501a0a696e9b12f46cc99eb9ae770b31/img/03_expense_update.png -------------------------------------------------------------------------------- /img/04_expense_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-htmx-tutorial/903c6e4c501a0a696e9b12f46cc99eb9ae770b31/img/04_expense_delete.png -------------------------------------------------------------------------------- /img/a02_combobox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-htmx-tutorial/903c6e4c501a0a696e9b12f46cc99eb9ae770b31/img/a02_combobox.png -------------------------------------------------------------------------------- /img/a03_tabela.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-htmx-tutorial/903c6e4c501a0a696e9b12f46cc99eb9ae770b31/img/a03_tabela.png -------------------------------------------------------------------------------- /img/expense_base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-htmx-tutorial/903c6e4c501a0a696e9b12f46cc99eb9ae770b31/img/expense_base.png -------------------------------------------------------------------------------- /img/htmx-2021-07-22-0947.excalidraw: -------------------------------------------------------------------------------- 1 | { 2 | "type": "excalidraw", 3 | "version": 2, 4 | "source": "https://excalidraw.com", 5 | "elements": [ 6 | { 7 | "type": "rectangle", 8 | "version": 136, 9 | "versionNonce": 1225414769, 10 | "isDeleted": false, 11 | "id": "10Y41qudlrCNb1YH1ygCG", 12 | "fillStyle": "hachure", 13 | "strokeWidth": 1, 14 | "strokeStyle": "solid", 15 | "roughness": 1, 16 | "opacity": 100, 17 | "angle": 0, 18 | "x": 392, 19 | "y": 121, 20 | "strokeColor": "#000000", 21 | "backgroundColor": "transparent", 22 | "width": 145.00000000000003, 23 | "height": 32.00000000000001, 24 | "seed": 411043946, 25 | "groupIds": [], 26 | "strokeSharpness": "sharp", 27 | "boundElementIds": [] 28 | }, 29 | { 30 | "type": "rectangle", 31 | "version": 495, 32 | "versionNonce": 1706713887, 33 | "isDeleted": false, 34 | "id": "gERmK16jePPipC3cVX8AN", 35 | "fillStyle": "solid", 36 | "strokeWidth": 1, 37 | "strokeStyle": "solid", 38 | "roughness": 1, 39 | "opacity": 100, 40 | "angle": 0, 41 | "x": 730, 42 | "y": 121.5, 43 | "strokeColor": "#000000", 44 | "backgroundColor": "#228be6", 45 | "width": 103.00000000000001, 46 | "height": 34, 47 | "seed": 1100822250, 48 | "groupIds": [ 49 | "xKa6h2qYCDILyKtyN0cVr" 50 | ], 51 | "strokeSharpness": "sharp", 52 | "boundElementIds": [ 53 | "R2fcLcJhxmPXWQTOJisdu", 54 | "2tbXGigm6sfs_RHdT7MzS", 55 | "PN4cPlI7xfz5dW72N5xoC" 56 | ] 57 | }, 58 | { 59 | "type": "text", 60 | "version": 480, 61 | "versionNonce": 870851153, 62 | "isDeleted": false, 63 | "id": "VZznXwb06nH1SMzH-KF8d", 64 | "fillStyle": "hachure", 65 | "strokeWidth": 1, 66 | "strokeStyle": "solid", 67 | "roughness": 1, 68 | "opacity": 100, 69 | "angle": 0, 70 | "x": 740, 71 | "y": 127.03488372093022, 72 | "strokeColor": "#000000", 73 | "backgroundColor": "#228be6", 74 | "width": 79, 75 | "height": 22.96511627906977, 76 | "seed": 455443638, 77 | "groupIds": [ 78 | "xKa6h2qYCDILyKtyN0cVr" 79 | ], 80 | "strokeSharpness": "sharp", 81 | "boundElementIds": [], 82 | "fontSize": 18.37209302325581, 83 | "fontFamily": 1, 84 | "text": "Adicionar", 85 | "baseline": 15.965116279069768, 86 | "textAlign": "center", 87 | "verticalAlign": "middle" 88 | }, 89 | { 90 | "type": "text", 91 | "version": 82, 92 | "versionNonce": 2011080511, 93 | "isDeleted": false, 94 | "id": "2_tSAK5VlUhc4ETn9BYW2", 95 | "fillStyle": "hachure", 96 | "strokeWidth": 1, 97 | "strokeStyle": "solid", 98 | "roughness": 1, 99 | "opacity": 100, 100 | "angle": 0, 101 | "x": 394, 102 | "y": 92, 103 | "strokeColor": "#000000", 104 | "backgroundColor": "#228be6", 105 | "width": 95, 106 | "height": 25, 107 | "seed": 1456619638, 108 | "groupIds": [], 109 | "strokeSharpness": "sharp", 110 | "boundElementIds": [], 111 | "fontSize": 20, 112 | "fontFamily": 1, 113 | "text": "Descrição", 114 | "baseline": 18, 115 | "textAlign": "left", 116 | "verticalAlign": "top" 117 | }, 118 | { 119 | "type": "rectangle", 120 | "version": 178, 121 | "versionNonce": 2016522289, 122 | "isDeleted": false, 123 | "id": "_gpmQsHFTZSnnw18XPvxK", 124 | "fillStyle": "hachure", 125 | "strokeWidth": 1, 126 | "strokeStyle": "solid", 127 | "roughness": 1, 128 | "opacity": 100, 129 | "angle": 0, 130 | "x": 558.5, 131 | "y": 120.5, 132 | "strokeColor": "#000000", 133 | "backgroundColor": "transparent", 134 | "width": 145.00000000000003, 135 | "height": 34.00000000000001, 136 | "seed": 880311850, 137 | "groupIds": [], 138 | "strokeSharpness": "sharp", 139 | "boundElementIds": [] 140 | }, 141 | { 142 | "type": "text", 143 | "version": 71, 144 | "versionNonce": 556099423, 145 | "isDeleted": false, 146 | "id": "JAde0p9dCWiNzXV9hGFPg", 147 | "fillStyle": "hachure", 148 | "strokeWidth": 1, 149 | "strokeStyle": "solid", 150 | "roughness": 1, 151 | "opacity": 100, 152 | "angle": 0, 153 | "x": 562, 154 | "y": 90, 155 | "strokeColor": "#000000", 156 | "backgroundColor": "#228be6", 157 | "width": 49, 158 | "height": 25, 159 | "seed": 523925814, 160 | "groupIds": [], 161 | "strokeSharpness": "sharp", 162 | "boundElementIds": [], 163 | "fontSize": 20, 164 | "fontFamily": 1, 165 | "text": "Valor", 166 | "baseline": 18, 167 | "textAlign": "left", 168 | "verticalAlign": "top" 169 | }, 170 | { 171 | "type": "line", 172 | "version": 472, 173 | "versionNonce": 2077131281, 174 | "isDeleted": false, 175 | "id": "T_4EWx1oxpIiMNkU875Ue", 176 | "fillStyle": "hachure", 177 | "strokeWidth": 1, 178 | "strokeStyle": "solid", 179 | "roughness": 1, 180 | "opacity": 100, 181 | "angle": 0, 182 | "x": 385.00000000000017, 183 | "y": 290, 184 | "strokeColor": "#000000", 185 | "backgroundColor": "#228be6", 186 | "width": 527.8221539251506, 187 | "height": 1.353028180636386, 188 | "seed": 1596231926, 189 | "groupIds": [], 190 | "strokeSharpness": "round", 191 | "boundElementIds": [], 192 | "startBinding": null, 193 | "endBinding": null, 194 | "lastCommittedPoint": null, 195 | "startArrowhead": null, 196 | "endArrowhead": null, 197 | "points": [ 198 | [ 199 | 0, 200 | 0 201 | ], 202 | [ 203 | 527.8221539251506, 204 | -1.353028180636386 205 | ] 206 | ] 207 | }, 208 | { 209 | "type": "line", 210 | "version": 560, 211 | "versionNonce": 1424200081, 212 | "isDeleted": false, 213 | "id": "16VbLMGGCYZMUidBDBkBR", 214 | "fillStyle": "hachure", 215 | "strokeWidth": 1, 216 | "strokeStyle": "solid", 217 | "roughness": 1, 218 | "opacity": 100, 219 | "angle": 0, 220 | "x": 1283.0889230374246, 221 | "y": 545.6765140903182, 222 | "strokeColor": "#000000", 223 | "backgroundColor": "#228be6", 224 | "width": 527.8221539251506, 225 | "height": 1.353028180636386, 226 | "seed": 1067409514, 227 | "groupIds": [], 228 | "strokeSharpness": "round", 229 | "boundElementIds": [], 230 | "startBinding": null, 231 | "endBinding": null, 232 | "lastCommittedPoint": null, 233 | "startArrowhead": null, 234 | "endArrowhead": null, 235 | "points": [ 236 | [ 237 | 0, 238 | 0 239 | ], 240 | [ 241 | 527.8221539251506, 242 | -1.353028180636386 243 | ] 244 | ] 245 | }, 246 | { 247 | "type": "line", 248 | "version": 571, 249 | "versionNonce": 168086527, 250 | "isDeleted": false, 251 | "id": "U3cXL6XP0xP6yO6r0Gk1b", 252 | "fillStyle": "hachure", 253 | "strokeWidth": 1, 254 | "strokeStyle": "solid", 255 | "roughness": 1, 256 | "opacity": 100, 257 | "angle": 0, 258 | "x": 1285.0889230374246, 259 | "y": 593.6765140903182, 260 | "strokeColor": "#000000", 261 | "backgroundColor": "#228be6", 262 | "width": 527.8221539251506, 263 | "height": 1.353028180636386, 264 | "seed": 349483830, 265 | "groupIds": [], 266 | "strokeSharpness": "round", 267 | "boundElementIds": [], 268 | "startBinding": null, 269 | "endBinding": null, 270 | "lastCommittedPoint": null, 271 | "startArrowhead": null, 272 | "endArrowhead": null, 273 | "points": [ 274 | [ 275 | 0, 276 | 0 277 | ], 278 | [ 279 | 527.8221539251506, 280 | -1.353028180636386 281 | ] 282 | ] 283 | }, 284 | { 285 | "type": "line", 286 | "version": 592, 287 | "versionNonce": 511522673, 288 | "isDeleted": false, 289 | "id": "g8L0XtYfHxcNoFjAVyqWU", 290 | "fillStyle": "hachure", 291 | "strokeWidth": 1, 292 | "strokeStyle": "solid", 293 | "roughness": 1, 294 | "opacity": 100, 295 | "angle": 0, 296 | "x": 1286.0889230374246, 297 | "y": 645.6765140903182, 298 | "strokeColor": "#000000", 299 | "backgroundColor": "#228be6", 300 | "width": 527.8221539251506, 301 | "height": 1.353028180636386, 302 | "seed": 1449902890, 303 | "groupIds": [], 304 | "strokeSharpness": "round", 305 | "boundElementIds": [], 306 | "startBinding": null, 307 | "endBinding": null, 308 | "lastCommittedPoint": null, 309 | "startArrowhead": null, 310 | "endArrowhead": null, 311 | "points": [ 312 | [ 313 | 0, 314 | 0 315 | ], 316 | [ 317 | 527.8221539251506, 318 | -1.353028180636386 319 | ] 320 | ] 321 | }, 322 | { 323 | "type": "text", 324 | "version": 206, 325 | "versionNonce": 580751295, 326 | "isDeleted": false, 327 | "id": "CLyv9u2gVNE_0-0MvlcvX", 328 | "fillStyle": "hachure", 329 | "strokeWidth": 1, 330 | "strokeStyle": "solid", 331 | "roughness": 1, 332 | "opacity": 100, 333 | "angle": 0, 334 | "x": 465, 335 | "y": 256, 336 | "strokeColor": "#000000", 337 | "backgroundColor": "#228be6", 338 | "width": 437, 339 | "height": 25, 340 | "seed": 1751814518, 341 | "groupIds": [], 342 | "strokeSharpness": "sharp", 343 | "boundElementIds": [], 344 | "fontSize": 20, 345 | "fontFamily": 1, 346 | "text": "Descrição Valor Pago Ações", 347 | "baseline": 18, 348 | "textAlign": "left", 349 | "verticalAlign": "top" 350 | }, 351 | { 352 | "type": "text", 353 | "version": 515, 354 | "versionNonce": 2091447327, 355 | "isDeleted": false, 356 | "id": "lceS204h-IQTV5mCeE7tc", 357 | "fillStyle": "hachure", 358 | "strokeWidth": 1, 359 | "strokeStyle": "solid", 360 | "roughness": 1, 361 | "opacity": 100, 362 | "angle": 0, 363 | "x": 1360, 364 | "y": 561.4999999999999, 365 | "strokeColor": "#000000", 366 | "backgroundColor": "#228be6", 367 | "width": 360, 368 | "height": 25, 369 | "seed": 1277375798, 370 | "groupIds": [], 371 | "strokeSharpness": "sharp", 372 | "boundElementIds": [], 373 | "fontSize": 20, 374 | "fontFamily": 1, 375 | "text": "Item 2 2,01 False", 376 | "baseline": 18, 377 | "textAlign": "left", 378 | "verticalAlign": "top" 379 | }, 380 | { 381 | "type": "text", 382 | "version": 492, 383 | "versionNonce": 900145489, 384 | "isDeleted": false, 385 | "id": "pboaWwzQ85LbiFoUD8JBw", 386 | "fillStyle": "hachure", 387 | "strokeWidth": 1, 388 | "strokeStyle": "solid", 389 | "roughness": 1, 390 | "opacity": 100, 391 | "angle": 0, 392 | "x": 1359, 393 | "y": 613.4999999999999, 394 | "strokeColor": "#000000", 395 | "backgroundColor": "#228be6", 396 | "width": 360, 397 | "height": 25, 398 | "seed": 1114481142, 399 | "groupIds": [], 400 | "strokeSharpness": "sharp", 401 | "boundElementIds": [], 402 | "fontSize": 20, 403 | "fontFamily": 1, 404 | "text": "Item 3 7,00 True", 405 | "baseline": 18, 406 | "textAlign": "left", 407 | "verticalAlign": "top" 408 | }, 409 | { 410 | "type": "text", 411 | "version": 44, 412 | "versionNonce": 281417585, 413 | "isDeleted": false, 414 | "id": "UcsefpaGOtFXWieK0-Cob", 415 | "fillStyle": "hachure", 416 | "strokeWidth": 1, 417 | "strokeStyle": "solid", 418 | "roughness": 1, 419 | "opacity": 100, 420 | "angle": 0, 421 | "x": 397, 422 | "y": 125, 423 | "strokeColor": "#c92a2a", 424 | "backgroundColor": "#228be6", 425 | "width": 95, 426 | "height": 25, 427 | "seed": 1638638698, 428 | "groupIds": [], 429 | "strokeSharpness": "sharp", 430 | "boundElementIds": [], 431 | "fontSize": 20, 432 | "fontFamily": 1, 433 | "text": "Novo item", 434 | "baseline": 18, 435 | "textAlign": "left", 436 | "verticalAlign": "top" 437 | }, 438 | { 439 | "type": "text", 440 | "version": 48, 441 | "versionNonce": 1474171935, 442 | "isDeleted": false, 443 | "id": "jocxgCKkpPfQjr4B0glUA", 444 | "fillStyle": "hachure", 445 | "strokeWidth": 1, 446 | "strokeStyle": "solid", 447 | "roughness": 1, 448 | "opacity": 100, 449 | "angle": 0, 450 | "x": 564, 451 | "y": 125, 452 | "strokeColor": "#c92a2a", 453 | "backgroundColor": "#228be6", 454 | "width": 50, 455 | "height": 25, 456 | "seed": 1259319466, 457 | "groupIds": [], 458 | "strokeSharpness": "sharp", 459 | "boundElementIds": [], 460 | "fontSize": 20, 461 | "fontFamily": 1, 462 | "text": "10,50", 463 | "baseline": 18, 464 | "textAlign": "center", 465 | "verticalAlign": "middle" 466 | }, 467 | { 468 | "id": "rVJ_G1gP0X3pDU9Z06Rog", 469 | "type": "rectangle", 470 | "x": 382, 471 | "y": 81, 472 | "width": 466, 473 | "height": 89, 474 | "angle": 0, 475 | "strokeColor": "#000000", 476 | "backgroundColor": "transparent", 477 | "fillStyle": "hachure", 478 | "strokeWidth": 1, 479 | "strokeStyle": "dashed", 480 | "roughness": 1, 481 | "opacity": 100, 482 | "groupIds": [], 483 | "strokeSharpness": "sharp", 484 | "seed": 1765997951, 485 | "version": 83, 486 | "versionNonce": 452228223, 487 | "isDeleted": false, 488 | "boundElementIds": [ 489 | "PN4cPlI7xfz5dW72N5xoC" 490 | ] 491 | }, 492 | { 493 | "type": "rectangle", 494 | "version": 449, 495 | "versionNonce": 1232359633, 496 | "isDeleted": false, 497 | "id": "vN8pij0G8nnJqowOJyL-6", 498 | "fillStyle": "solid", 499 | "strokeWidth": 1, 500 | "strokeStyle": "solid", 501 | "roughness": 1, 502 | "opacity": 100, 503 | "angle": 0, 504 | "x": 407, 505 | "y": 182.5, 506 | "strokeColor": "#087f5b", 507 | "backgroundColor": "#fff", 508 | "width": 90, 509 | "height": 36, 510 | "seed": 707340433, 511 | "groupIds": [ 512 | "CvBOBkQyoDZ9dwqlJL-4P" 513 | ], 514 | "strokeSharpness": "sharp", 515 | "boundElementIds": [] 516 | }, 517 | { 518 | "type": "text", 519 | "version": 410, 520 | "versionNonce": 1934794943, 521 | "isDeleted": false, 522 | "id": "hUTV3u3ykM5l9cC9jXP9q", 523 | "fillStyle": "hachure", 524 | "strokeWidth": 1, 525 | "strokeStyle": "solid", 526 | "roughness": 1, 527 | "opacity": 100, 528 | "angle": 0, 529 | "x": 429, 530 | "y": 188, 531 | "strokeColor": "#087f5b", 532 | "backgroundColor": "transparent", 533 | "width": 48, 534 | "height": 25, 535 | "seed": 1187521791, 536 | "groupIds": [ 537 | "CvBOBkQyoDZ9dwqlJL-4P" 538 | ], 539 | "strokeSharpness": "sharp", 540 | "boundElementIds": [], 541 | "fontSize": 20, 542 | "fontFamily": 1, 543 | "text": "Pago", 544 | "baseline": 18, 545 | "textAlign": "center", 546 | "verticalAlign": "middle" 547 | }, 548 | { 549 | "type": "rectangle", 550 | "version": 521, 551 | "versionNonce": 422842033, 552 | "isDeleted": false, 553 | "id": "TnZNItjgWkg0mBCmlVSH4", 554 | "fillStyle": "solid", 555 | "strokeWidth": 1, 556 | "strokeStyle": "solid", 557 | "roughness": 1, 558 | "opacity": 100, 559 | "angle": 0, 560 | "x": 512, 561 | "y": 183.5, 562 | "strokeColor": "#c92a2a", 563 | "backgroundColor": "#fff", 564 | "width": 107, 565 | "height": 37, 566 | "seed": 1487384849, 567 | "groupIds": [ 568 | "RaQZNc5TnYrUP4DmhlAjZ" 569 | ], 570 | "strokeSharpness": "sharp", 571 | "boundElementIds": [ 572 | "PN4cPlI7xfz5dW72N5xoC" 573 | ] 574 | }, 575 | { 576 | "type": "text", 577 | "version": 422, 578 | "versionNonce": 1323184351, 579 | "isDeleted": false, 580 | "id": "8YubyPXpvLzrAFZbaaJBG", 581 | "fillStyle": "hachure", 582 | "strokeWidth": 1, 583 | "strokeStyle": "solid", 584 | "roughness": 1, 585 | "opacity": 100, 586 | "angle": 0, 587 | "x": 518.5, 588 | "y": 191, 589 | "strokeColor": "#c92a2a", 590 | "backgroundColor": "transparent", 591 | "width": 95, 592 | "height": 25, 593 | "seed": 733242495, 594 | "groupIds": [ 595 | "RaQZNc5TnYrUP4DmhlAjZ" 596 | ], 597 | "strokeSharpness": "sharp", 598 | "boundElementIds": [], 599 | "fontSize": 20, 600 | "fontFamily": 1, 601 | "text": "Não Pago", 602 | "baseline": 18, 603 | "textAlign": "center", 604 | "verticalAlign": "middle" 605 | }, 606 | { 607 | "type": "rectangle", 608 | "version": 781, 609 | "versionNonce": 886249535, 610 | "isDeleted": false, 611 | "id": "V9tAH9QwvO1iBIxa2X_t1", 612 | "fillStyle": "solid", 613 | "strokeWidth": 1, 614 | "strokeStyle": "solid", 615 | "roughness": 1, 616 | "opacity": 100, 617 | "angle": 0, 618 | "x": 1296.2499999999995, 619 | "y": 556.2499999999999, 620 | "strokeColor": "#000000", 621 | "backgroundColor": "#fff", 622 | "width": 27, 623 | "height": 27, 624 | "seed": 2008112511, 625 | "groupIds": [ 626 | "aPkLIiSvCzIvTiIj86EM4" 627 | ], 628 | "strokeSharpness": "sharp", 629 | "boundElementIds": [] 630 | }, 631 | { 632 | "type": "rectangle", 633 | "version": 788, 634 | "versionNonce": 129552177, 635 | "isDeleted": false, 636 | "id": "8oqIPbIhFAkXxmSAAd55s", 637 | "fillStyle": "solid", 638 | "strokeWidth": 1, 639 | "strokeStyle": "solid", 640 | "roughness": 1, 641 | "opacity": 100, 642 | "angle": 0, 643 | "x": 1294.2499999999995, 644 | "y": 609.2499999999999, 645 | "strokeColor": "#000000", 646 | "backgroundColor": "#fff", 647 | "width": 27, 648 | "height": 27, 649 | "seed": 2043106719, 650 | "groupIds": [ 651 | "vQk-Ib_M7frosZU7Ip_vq" 652 | ], 653 | "strokeSharpness": "sharp", 654 | "boundElementIds": [] 655 | }, 656 | { 657 | "type": "rectangle", 658 | "version": 949, 659 | "versionNonce": 140752991, 660 | "isDeleted": false, 661 | "id": "ECsTC3t7asqnUrYdjsFXB", 662 | "fillStyle": "solid", 663 | "strokeWidth": 1, 664 | "strokeStyle": "solid", 665 | "roughness": 1, 666 | "opacity": 100, 667 | "angle": 0, 668 | "x": 1734.5874958261102, 669 | "y": 555.9999999999999, 670 | "strokeColor": "#0b7285", 671 | "backgroundColor": "#fff", 672 | "width": 34.000000000000014, 673 | "height": 27.000000000000004, 674 | "seed": 1885067327, 675 | "groupIds": [ 676 | "HkspBm5LqZEVbhjR8qk5a" 677 | ], 678 | "strokeSharpness": "sharp", 679 | "boundElementIds": [ 680 | "781OPTwOynS37eFZMMQ5F" 681 | ] 682 | }, 683 | { 684 | "type": "text", 685 | "version": 862, 686 | "versionNonce": 1796849937, 687 | "isDeleted": false, 688 | "id": "HYElqe85spQNNbWm5VWnc", 689 | "fillStyle": "hachure", 690 | "strokeWidth": 1, 691 | "strokeStyle": "solid", 692 | "roughness": 1, 693 | "opacity": 100, 694 | "angle": 0, 695 | "x": 1746.0874958261102, 696 | "y": 554.4999999999999, 697 | "strokeColor": "#0b7285", 698 | "backgroundColor": "transparent", 699 | "width": 11, 700 | "height": 25, 701 | "seed": 907808561, 702 | "groupIds": [ 703 | "HkspBm5LqZEVbhjR8qk5a" 704 | ], 705 | "strokeSharpness": "sharp", 706 | "boundElementIds": [ 707 | "FYl2L8HtKq74zLXlJO5Fv" 708 | ], 709 | "fontSize": 20, 710 | "fontFamily": 1, 711 | "text": "e", 712 | "baseline": 18, 713 | "textAlign": "center", 714 | "verticalAlign": "middle" 715 | }, 716 | { 717 | "type": "rectangle", 718 | "version": 981, 719 | "versionNonce": 1026249855, 720 | "isDeleted": false, 721 | "id": "CqrrI2WShDZo1p7BS0Cfm", 722 | "fillStyle": "solid", 723 | "strokeWidth": 1, 724 | "strokeStyle": "solid", 725 | "roughness": 1, 726 | "opacity": 100, 727 | "angle": 0, 728 | "x": 1736.5874958261102, 729 | "y": 603.7499999999999, 730 | "strokeColor": "#0b7285", 731 | "backgroundColor": "#fff", 732 | "width": 34.000000000000014, 733 | "height": 27.000000000000004, 734 | "seed": 778527167, 735 | "groupIds": [ 736 | "KtpQh_ZyaD8bRFfi7y-ko" 737 | ], 738 | "strokeSharpness": "sharp", 739 | "boundElementIds": [] 740 | }, 741 | { 742 | "type": "text", 743 | "version": 893, 744 | "versionNonce": 2101442289, 745 | "isDeleted": false, 746 | "id": "vSLmjBfb2XMQo0SxZ0mvq", 747 | "fillStyle": "hachure", 748 | "strokeWidth": 1, 749 | "strokeStyle": "solid", 750 | "roughness": 1, 751 | "opacity": 100, 752 | "angle": 0, 753 | "x": 1748.0874958261102, 754 | "y": 602.2499999999999, 755 | "strokeColor": "#0b7285", 756 | "backgroundColor": "transparent", 757 | "width": 11, 758 | "height": 25, 759 | "seed": 1468521905, 760 | "groupIds": [ 761 | "KtpQh_ZyaD8bRFfi7y-ko" 762 | ], 763 | "strokeSharpness": "sharp", 764 | "boundElementIds": [], 765 | "fontSize": 20, 766 | "fontFamily": 1, 767 | "text": "e", 768 | "baseline": 18, 769 | "textAlign": "center", 770 | "verticalAlign": "middle" 771 | }, 772 | { 773 | "id": "iR39iO2O8TsixfyXvMJWq", 774 | "type": "rectangle", 775 | "x": 1116.5874958261102, 776 | "y": 455.5, 777 | "width": 537, 778 | "height": 35, 779 | "angle": 0, 780 | "strokeColor": "#e67700", 781 | "backgroundColor": "#fab005", 782 | "fillStyle": "hachure", 783 | "strokeWidth": 1, 784 | "strokeStyle": "dashed", 785 | "roughness": 1, 786 | "opacity": 100, 787 | "groupIds": [ 788 | "KKdBFm11XvzoC0wdMygS0" 789 | ], 790 | "strokeSharpness": "sharp", 791 | "seed": 174819409, 792 | "version": 461, 793 | "versionNonce": 465562431, 794 | "isDeleted": false, 795 | "boundElementIds": [ 796 | "s9w4ISdiTk7ZaNzkn7XVg" 797 | ] 798 | }, 799 | { 800 | "type": "text", 801 | "version": 435, 802 | "versionNonce": 662879647, 803 | "isDeleted": false, 804 | "id": "JlkR0icPAU3XCDYwrVkYi", 805 | "fillStyle": "hachure", 806 | "strokeWidth": 1, 807 | "strokeStyle": "solid", 808 | "roughness": 1, 809 | "opacity": 100, 810 | "angle": 0, 811 | "x": 1188, 812 | "y": 465, 813 | "strokeColor": "#000000", 814 | "backgroundColor": "#228be6", 815 | "width": 353, 816 | "height": 25, 817 | "seed": 1287722678, 818 | "groupIds": [ 819 | "KKdBFm11XvzoC0wdMygS0" 820 | ], 821 | "strokeSharpness": "sharp", 822 | "boundElementIds": [], 823 | "fontSize": 20, 824 | "fontFamily": 1, 825 | "text": "Item 1 1,99 True", 826 | "baseline": 18, 827 | "textAlign": "left", 828 | "verticalAlign": "top" 829 | }, 830 | { 831 | "type": "rectangle", 832 | "version": 789, 833 | "versionNonce": 357085137, 834 | "isDeleted": false, 835 | "id": "8qTrhV2Fryix_yNihKKmW", 836 | "fillStyle": "solid", 837 | "strokeWidth": 1, 838 | "strokeStyle": "solid", 839 | "roughness": 1, 840 | "opacity": 100, 841 | "angle": 0, 842 | "x": 1122.0874958261102, 843 | "y": 460.99999999999994, 844 | "strokeColor": "#000000", 845 | "backgroundColor": "#fff", 846 | "width": 27, 847 | "height": 27, 848 | "seed": 674642065, 849 | "groupIds": [ 850 | "7XLtzMqGrKo-cRoolevHf", 851 | "KKdBFm11XvzoC0wdMygS0" 852 | ], 853 | "strokeSharpness": "sharp", 854 | "boundElementIds": [] 855 | }, 856 | { 857 | "type": "rectangle", 858 | "version": 971, 859 | "versionNonce": 1152889279, 860 | "isDeleted": false, 861 | "id": "zfF_jojPsBY_RLBeS31mI", 862 | "fillStyle": "solid", 863 | "strokeWidth": 1, 864 | "strokeStyle": "solid", 865 | "roughness": 1, 866 | "opacity": 100, 867 | "angle": 0, 868 | "x": 1564.5874958261102, 869 | "y": 459.75, 870 | "strokeColor": "#0b7285", 871 | "backgroundColor": "#fff", 872 | "width": 34.000000000000014, 873 | "height": 27.000000000000004, 874 | "seed": 1042577521, 875 | "groupIds": [ 876 | "Ifg6vUfQ2nHrxO1nADFIY", 877 | "KKdBFm11XvzoC0wdMygS0" 878 | ], 879 | "strokeSharpness": "sharp", 880 | "boundElementIds": [ 881 | "781OPTwOynS37eFZMMQ5F", 882 | "NljL3LrjLL1m8zIk6F2QO" 883 | ] 884 | }, 885 | { 886 | "type": "text", 887 | "version": 883, 888 | "versionNonce": 1227132337, 889 | "isDeleted": false, 890 | "id": "uHzyNz1ENW-D6REORTiwf", 891 | "fillStyle": "hachure", 892 | "strokeWidth": 1, 893 | "strokeStyle": "solid", 894 | "roughness": 1, 895 | "opacity": 100, 896 | "angle": 0, 897 | "x": 1576.0874958261102, 898 | "y": 458.25, 899 | "strokeColor": "#0b7285", 900 | "backgroundColor": "transparent", 901 | "width": 11, 902 | "height": 25, 903 | "seed": 2127708959, 904 | "groupIds": [ 905 | "Ifg6vUfQ2nHrxO1nADFIY", 906 | "KKdBFm11XvzoC0wdMygS0" 907 | ], 908 | "strokeSharpness": "sharp", 909 | "boundElementIds": [ 910 | "FYl2L8HtKq74zLXlJO5Fv" 911 | ], 912 | "fontSize": 20, 913 | "fontFamily": 1, 914 | "text": "e", 915 | "baseline": 18, 916 | "textAlign": "center", 917 | "verticalAlign": "middle" 918 | }, 919 | { 920 | "type": "rectangle", 921 | "version": 966, 922 | "versionNonce": 863576543, 923 | "isDeleted": false, 924 | "id": "aaQs15OvO0Nx-NJPhwfrM", 925 | "fillStyle": "solid", 926 | "strokeWidth": 1, 927 | "strokeStyle": "solid", 928 | "roughness": 1, 929 | "opacity": 100, 930 | "angle": 0, 931 | "x": 1608.5874958261102, 932 | "y": 459.75, 933 | "strokeColor": "#c92a2a", 934 | "backgroundColor": "#fff", 935 | "width": 34.000000000000014, 936 | "height": 27.000000000000004, 937 | "seed": 491771103, 938 | "groupIds": [ 939 | "vXatbHh0P8j6Ox-vJEwJT", 940 | "KKdBFm11XvzoC0wdMygS0" 941 | ], 942 | "strokeSharpness": "sharp", 943 | "boundElementIds": [ 944 | "781OPTwOynS37eFZMMQ5F", 945 | "s9w4ISdiTk7ZaNzkn7XVg" 946 | ] 947 | }, 948 | { 949 | "type": "text", 950 | "version": 878, 951 | "versionNonce": 11507057, 952 | "isDeleted": false, 953 | "id": "FD3WkENrNSfoPeOXM-qk7", 954 | "fillStyle": "hachure", 955 | "strokeWidth": 1, 956 | "strokeStyle": "solid", 957 | "roughness": 1, 958 | "opacity": 100, 959 | "angle": 0, 960 | "x": 1620.0874958261102, 961 | "y": 458.25, 962 | "strokeColor": "#c92a2a", 963 | "backgroundColor": "transparent", 964 | "width": 11, 965 | "height": 25, 966 | "seed": 2146151569, 967 | "groupIds": [ 968 | "vXatbHh0P8j6Ox-vJEwJT", 969 | "KKdBFm11XvzoC0wdMygS0" 970 | ], 971 | "strokeSharpness": "sharp", 972 | "boundElementIds": [ 973 | "FYl2L8HtKq74zLXlJO5Fv" 974 | ], 975 | "fontSize": 20, 976 | "fontFamily": 1, 977 | "text": "d", 978 | "baseline": 18, 979 | "textAlign": "center", 980 | "verticalAlign": "middle" 981 | }, 982 | { 983 | "type": "rectangle", 984 | "version": 1020, 985 | "versionNonce": 183135391, 986 | "isDeleted": false, 987 | "id": "pYPQMAJoIzm85JpWCdmCQ", 988 | "fillStyle": "solid", 989 | "strokeWidth": 1, 990 | "strokeStyle": "solid", 991 | "roughness": 1, 992 | "opacity": 100, 993 | "angle": 0, 994 | "x": 1776.5874958261102, 995 | "y": 553.7499999999999, 996 | "strokeColor": "#000000", 997 | "backgroundColor": "#fa5252", 998 | "width": 34.000000000000014, 999 | "height": 27.000000000000004, 1000 | "seed": 1917886737, 1001 | "groupIds": [ 1002 | "FySIaQXhFxYxGbf_B5PmK" 1003 | ], 1004 | "strokeSharpness": "sharp", 1005 | "boundElementIds": [ 1006 | "781OPTwOynS37eFZMMQ5F", 1007 | "11WbkAuvKYiquFcBIHha5" 1008 | ] 1009 | }, 1010 | { 1011 | "type": "text", 1012 | "version": 931, 1013 | "versionNonce": 1085763793, 1014 | "isDeleted": false, 1015 | "id": "fslQxAJqW_klJ4tr8VCs4", 1016 | "fillStyle": "hachure", 1017 | "strokeWidth": 1, 1018 | "strokeStyle": "solid", 1019 | "roughness": 1, 1020 | "opacity": 100, 1021 | "angle": 0, 1022 | "x": 1788.0874958261102, 1023 | "y": 552.2499999999999, 1024 | "strokeColor": "#000000", 1025 | "backgroundColor": "#fa5252", 1026 | "width": 11, 1027 | "height": 25, 1028 | "seed": 290616447, 1029 | "groupIds": [ 1030 | "FySIaQXhFxYxGbf_B5PmK" 1031 | ], 1032 | "strokeSharpness": "sharp", 1033 | "boundElementIds": [ 1034 | "FYl2L8HtKq74zLXlJO5Fv" 1035 | ], 1036 | "fontSize": 20, 1037 | "fontFamily": 1, 1038 | "text": "d", 1039 | "baseline": 18, 1040 | "textAlign": "center", 1041 | "verticalAlign": "middle" 1042 | }, 1043 | { 1044 | "type": "rectangle", 1045 | "version": 1026, 1046 | "versionNonce": 1565622463, 1047 | "isDeleted": false, 1048 | "id": "KaERjaW1IwpnTDwf52d6p", 1049 | "fillStyle": "solid", 1050 | "strokeWidth": 1, 1051 | "strokeStyle": "solid", 1052 | "roughness": 1, 1053 | "opacity": 100, 1054 | "angle": 0, 1055 | "x": 1779.5874958261102, 1056 | "y": 604.7499999999999, 1057 | "strokeColor": "#c92a2a", 1058 | "backgroundColor": "#fff", 1059 | "width": 34.000000000000014, 1060 | "height": 27.000000000000004, 1061 | "seed": 1337348305, 1062 | "groupIds": [ 1063 | "Pc2-2rqhqSzhfN8j0SjXe" 1064 | ], 1065 | "strokeSharpness": "sharp", 1066 | "boundElementIds": [ 1067 | "781OPTwOynS37eFZMMQ5F" 1068 | ] 1069 | }, 1070 | { 1071 | "type": "text", 1072 | "version": 940, 1073 | "versionNonce": 2064854705, 1074 | "isDeleted": false, 1075 | "id": "qZi8d6gLfm21bBKG8Y5rl", 1076 | "fillStyle": "hachure", 1077 | "strokeWidth": 1, 1078 | "strokeStyle": "solid", 1079 | "roughness": 1, 1080 | "opacity": 100, 1081 | "angle": 0, 1082 | "x": 1791.0874958261102, 1083 | "y": 603.2499999999999, 1084 | "strokeColor": "#c92a2a", 1085 | "backgroundColor": "transparent", 1086 | "width": 11, 1087 | "height": 25, 1088 | "seed": 1781571775, 1089 | "groupIds": [ 1090 | "Pc2-2rqhqSzhfN8j0SjXe" 1091 | ], 1092 | "strokeSharpness": "sharp", 1093 | "boundElementIds": [ 1094 | "FYl2L8HtKq74zLXlJO5Fv" 1095 | ], 1096 | "fontSize": 20, 1097 | "fontFamily": 1, 1098 | "text": "d", 1099 | "baseline": 18, 1100 | "textAlign": "center", 1101 | "verticalAlign": "middle" 1102 | }, 1103 | { 1104 | "id": "CxB9TOXZpyqM5edpyzWMy", 1105 | "type": "rectangle", 1106 | "x": 339.5874958261103, 1107 | "y": 56.5, 1108 | "width": 627, 1109 | "height": 491, 1110 | "angle": 0, 1111 | "strokeColor": "#000000", 1112 | "backgroundColor": "transparent", 1113 | "fillStyle": "hachure", 1114 | "strokeWidth": 1, 1115 | "strokeStyle": "solid", 1116 | "roughness": 1, 1117 | "opacity": 100, 1118 | "groupIds": [], 1119 | "strokeSharpness": "sharp", 1120 | "seed": 1532587057, 1121 | "version": 83, 1122 | "versionNonce": 1027811103, 1123 | "isDeleted": false, 1124 | "boundElementIds": null 1125 | }, 1126 | { 1127 | "id": "diglHs9rERWzqgvyt2llw", 1128 | "type": "rectangle", 1129 | "x": 1065.5874958261102, 1130 | "y": 179.5, 1131 | "width": 571, 1132 | "height": 195, 1133 | "angle": 0, 1134 | "strokeColor": "#2b8a3e", 1135 | "backgroundColor": "transparent", 1136 | "fillStyle": "hachure", 1137 | "strokeWidth": 1, 1138 | "strokeStyle": "dashed", 1139 | "roughness": 1, 1140 | "opacity": 100, 1141 | "groupIds": [], 1142 | "strokeSharpness": "sharp", 1143 | "seed": 76364927, 1144 | "version": 573, 1145 | "versionNonce": 670773233, 1146 | "isDeleted": false, 1147 | "boundElementIds": null 1148 | }, 1149 | { 1150 | "id": "uT_0YLmm4HyEv1Q52FbmO", 1151 | "type": "text", 1152 | "x": 1064.5874958261102, 1153 | "y": 383.5, 1154 | "width": 384, 1155 | "height": 25, 1156 | "angle": 0, 1157 | "strokeColor": "#2b8a3e", 1158 | "backgroundColor": "transparent", 1159 | "fillStyle": "hachure", 1160 | "strokeWidth": 1, 1161 | "strokeStyle": "dashed", 1162 | "roughness": 1, 1163 | "opacity": 100, 1164 | "groupIds": [], 1165 | "strokeSharpness": "sharp", 1166 | "seed": 193885055, 1167 | "version": 308, 1168 | "versionNonce": 1773071167, 1169 | "isDeleted": false, 1170 | "boundElementIds": null, 1171 | "text": "expense_table.html (laço de repetição)", 1172 | "fontSize": 20, 1173 | "fontFamily": 1, 1174 | "textAlign": "left", 1175 | "verticalAlign": "top", 1176 | "baseline": 18 1177 | }, 1178 | { 1179 | "id": "rQdAibvHTFUJjfQd5RCrA", 1180 | "type": "text", 1181 | "x": 346.5874958261103, 1182 | "y": 552.5, 1183 | "width": 167, 1184 | "height": 25, 1185 | "angle": 0, 1186 | "strokeColor": "#000000", 1187 | "backgroundColor": "transparent", 1188 | "fillStyle": "hachure", 1189 | "strokeWidth": 1, 1190 | "strokeStyle": "dashed", 1191 | "roughness": 1, 1192 | "opacity": 100, 1193 | "groupIds": [], 1194 | "strokeSharpness": "sharp", 1195 | "seed": 1415519697, 1196 | "version": 66, 1197 | "versionNonce": 2104994865, 1198 | "isDeleted": false, 1199 | "boundElementIds": null, 1200 | "text": "expense_list.html", 1201 | "fontSize": 20, 1202 | "fontFamily": 1, 1203 | "textAlign": "left", 1204 | "verticalAlign": "top", 1205 | "baseline": 18 1206 | }, 1207 | { 1208 | "id": "ZWN67IQjB8UPR38HsbsDH", 1209 | "type": "text", 1210 | "x": 1669.5874958261102, 1211 | "y": 394.5, 1212 | "width": 194, 1213 | "height": 25, 1214 | "angle": 0, 1215 | "strokeColor": "#000000", 1216 | "backgroundColor": "transparent", 1217 | "fillStyle": "hachure", 1218 | "strokeWidth": 1, 1219 | "strokeStyle": "dashed", 1220 | "roughness": 1, 1221 | "opacity": 100, 1222 | "groupIds": [], 1223 | "strokeSharpness": "sharp", 1224 | "seed": 865202111, 1225 | "version": 181, 1226 | "versionNonce": 494151455, 1227 | "isDeleted": false, 1228 | "boundElementIds": [ 1229 | "s9w4ISdiTk7ZaNzkn7XVg" 1230 | ], 1231 | "text": "expense_result.html", 1232 | "fontSize": 20, 1233 | "fontFamily": 1, 1234 | "textAlign": "left", 1235 | "verticalAlign": "top", 1236 | "baseline": 18 1237 | }, 1238 | { 1239 | "id": "s9w4ISdiTk7ZaNzkn7XVg", 1240 | "type": "arrow", 1241 | "x": 1700.3633083668658, 1242 | "y": 427.49999999999994, 1243 | "width": 38.319026030388386, 1244 | "height": 29.698536164149857, 1245 | "angle": 0, 1246 | "strokeColor": "#000000", 1247 | "backgroundColor": "transparent", 1248 | "fillStyle": "hachure", 1249 | "strokeWidth": 1, 1250 | "strokeStyle": "solid", 1251 | "roughness": 1, 1252 | "opacity": 100, 1253 | "groupIds": [], 1254 | "strokeSharpness": "round", 1255 | "seed": 1193566033, 1256 | "version": 1073, 1257 | "versionNonce": 1084005969, 1258 | "isDeleted": false, 1259 | "boundElementIds": null, 1260 | "points": [ 1261 | [ 1262 | 0, 1263 | 0 1264 | ], 1265 | [ 1266 | -38.319026030388386, 1267 | 29.698536164149857 1268 | ] 1269 | ], 1270 | "lastCommittedPoint": null, 1271 | "startBinding": { 1272 | "elementId": "ZWN67IQjB8UPR38HsbsDH", 1273 | "focus": 0.35158058114645896, 1274 | "gap": 7.999999999999943 1275 | }, 1276 | "endBinding": { 1277 | "elementId": "iR39iO2O8TsixfyXvMJWq", 1278 | "focus": 0.8814380367983794, 1279 | "gap": 8.456786510367237 1280 | }, 1281 | "startArrowhead": null, 1282 | "endArrowhead": "arrow" 1283 | } 1284 | ], 1285 | "appState": { 1286 | "gridSize": null, 1287 | "viewBackgroundColor": "#ffffff" 1288 | } 1289 | } -------------------------------------------------------------------------------- /img/htmx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/django-htmx-tutorial/903c6e4c501a0a696e9b12f46cc99eb9ae770b31/img/htmx.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | htmx 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 30 | 31 | 32 |

Lista de Despesas (Client side)

33 |

Consumindo API Rest

34 | 35 |
36 | 37 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 72 | 73 |
DescriçãoValorPago
74 |
75 | 76 | -------------------------------------------------------------------------------- /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-cors-headers==3.7.0 3 | django-extensions==3.1.3 4 | django-localflavor==3.1 5 | Django==3.2.* 6 | Faker==8.10.1 7 | isort==5.9.2 8 | python-decouple==3.4 9 | --------------------------------------------------------------------------------