├── backend
├── __init__.py
├── core
│ ├── __init__.py
│ ├── migrations
│ │ └── __init__.py
│ ├── templatetags
│ │ ├── __init__.py
│ │ ├── url_replace.py
│ │ └── rows_tags.py
│ ├── static
│ │ ├── img
│ │ │ └── django-logo-negative.png
│ │ └── css
│ │ │ ├── table.css
│ │ │ ├── style.css
│ │ │ └── icons
│ │ │ └── simple-line-icons.min.css
│ ├── views.py
│ ├── apps.py
│ ├── urls.py
│ ├── templates
│ │ ├── index.html
│ │ ├── includes
│ │ │ ├── nav.html
│ │ │ └── pagination.html
│ │ └── base.html
│ └── models.py
├── expense
│ ├── __init__.py
│ ├── migrations
│ │ ├── __init__.py
│ │ └── 0001_initial.py
│ ├── templates
│ │ ├── expense
│ │ │ ├── expense_table.html
│ │ │ ├── expense_content.html
│ │ │ └── expense_list.html
│ │ └── includes
│ │ │ ├── ordering_field.html
│ │ │ └── ordering_icon.html
│ ├── apps.py
│ ├── urls.py
│ ├── admin.py
│ ├── models.py
│ └── views.py
├── urls.py
├── asgi.py
├── wsgi.py
└── settings.py
├── requirements.txt
├── manage.py
├── contrib
└── env_gen.py
├── README.md
└── .gitignore
/backend/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/core/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/expense/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/core/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/core/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/expense/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/core/static/img/django-logo-negative.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rg3915/django-datatables-htmx/main/backend/core/static/img/django-logo-negative.png
--------------------------------------------------------------------------------
/backend/core/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 |
3 |
4 | def index(request):
5 | template_name = 'index.html'
6 | return render(request, template_name)
7 |
--------------------------------------------------------------------------------
/backend/expense/templates/expense/expense_table.html:
--------------------------------------------------------------------------------
1 |
2 | {% for object in object_list %}
3 | {% include "./expense_content.html" %}
4 | {% endfor %}
5 |
--------------------------------------------------------------------------------
/backend/core/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class CoreConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'backend.core'
7 |
--------------------------------------------------------------------------------
/backend/expense/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class ExpenseConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'backend.expense'
7 |
--------------------------------------------------------------------------------
/backend/core/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from backend.core import views as v
4 |
5 | app_name = 'core'
6 |
7 |
8 | urlpatterns = [
9 | path('', v.index, name='index'),
10 | ]
11 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | dj-database-url==0.5.0
2 | django-extensions==3.1.5
3 | django-seed==0.3.1
4 | django-widget-tweaks==1.4.9
5 | Django==4.0
6 | djhtml==1.4.11
7 | isort==5.10.1
8 | psycopg2-binary
9 | python-decouple==3.5
10 |
--------------------------------------------------------------------------------
/backend/expense/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from backend.expense import views as v
4 |
5 | app_name = 'expense'
6 |
7 |
8 | urlpatterns = [
9 | path('', v.ExpenseListView.as_view(), name='expense_list'),
10 | ]
11 |
--------------------------------------------------------------------------------
/backend/expense/templates/expense/expense_content.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ object.id }}
4 | {{ object.description }}
5 | {{ object.value }}
6 | {{ object.payment_date|date:'d/m/Y'|default:'---' }}
7 |
8 |
--------------------------------------------------------------------------------
/backend/urls.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.urls import include, path
3 |
4 | urlpatterns = [
5 | path('', include('backend.core.urls', namespace='core')),
6 | path('expense/', include('backend.expense.urls', namespace='expense')),
7 | path('admin/', admin.site.urls),
8 | ]
9 |
--------------------------------------------------------------------------------
/backend/expense/templates/includes/ordering_field.html:
--------------------------------------------------------------------------------
1 | {% load url_replace %}
2 |
3 |
8 | {{ field.label }}
9 |
10 |
--------------------------------------------------------------------------------
/backend/expense/templates/includes/ordering_icon.html:
--------------------------------------------------------------------------------
1 |
2 | {% if order == ordering %}
3 | {% if '-' in order %}
4 |
5 | {% else %}
6 |
7 | {% endif %}
8 | {% else %}
9 |
10 | {% endif %}
11 |
12 |
--------------------------------------------------------------------------------
/backend/expense/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from .models import Expense
4 |
5 |
6 | @admin.register(Expense)
7 | class ExpenseAdmin(admin.ModelAdmin):
8 | list_display = ('__str__', 'value', 'payment_date', 'paid')
9 | search_fields = ('description',)
10 | list_filter = ('paid',)
11 | date_hierarchy = 'payment_date'
12 |
--------------------------------------------------------------------------------
/backend/core/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 | {% extends "base.html" %}
3 |
4 | {% block content %}
5 |
6 |
Django datatables htmx
7 |
Exemplo de datatables feito apenas com htmx.
8 |
Veja no Github .
9 |
10 | {% endblock content %}
11 |
--------------------------------------------------------------------------------
/backend/core/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 |
4 | class TimeStampedModel(models.Model):
5 | created = models.DateTimeField(
6 | 'criado em',
7 | auto_now_add=True,
8 | auto_now=False
9 | )
10 | modified = models.DateTimeField(
11 | 'modificado em',
12 | auto_now_add=False,
13 | auto_now=True
14 | )
15 |
16 | class Meta:
17 | abstract = True
18 |
--------------------------------------------------------------------------------
/backend/core/templatetags/url_replace.py:
--------------------------------------------------------------------------------
1 | from django import template
2 |
3 | register = template.Library()
4 |
5 |
6 | @register.simple_tag(takes_context=True)
7 | def url_replace(context, **kwargs):
8 | query = context['request'].GET.copy()
9 | query.pop('rows_per_page', None)
10 | query.pop('page', None)
11 | query.pop('search', None)
12 | query.pop('sort_by', None)
13 | query.update(kwargs)
14 | return query.urlencode()
15 |
--------------------------------------------------------------------------------
/backend/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI config for backend project.
3 |
4 | It exposes the ASGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.asgi import get_asgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
15 |
16 | application = get_asgi_application()
17 |
--------------------------------------------------------------------------------
/backend/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for backend project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/backend/core/static/css/table.css:
--------------------------------------------------------------------------------
1 | .dt-panel {
2 | width: 100%;
3 | display: flex;
4 | align-items: center;
5 | }
6 |
7 | .dt-left-panel {
8 | width: 10%;
9 | flex-direction: row;
10 | align-items: center;
11 | }
12 |
13 | .dt-right-panel {
14 | width: 50%;
15 | margin-left: auto;
16 | align-items: flex-end;
17 | }
18 |
19 | .dt-total-panel {
20 | width: 20%;
21 | }
22 |
23 | .dt-pagination {
24 | display: flex;
25 | align-items: center;
26 | justify-content: flex-end;
27 | }
--------------------------------------------------------------------------------
/backend/expense/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | from backend.core.models import TimeStampedModel
4 |
5 |
6 | class Expense(TimeStampedModel):
7 | description = models.CharField('descrição', max_length=50)
8 | payment_date = models.DateField('data de pagamento', null=True, blank=True)
9 | value = models.DecimalField('valor', max_digits=7, decimal_places=2)
10 | paid = models.BooleanField('pago', default=False)
11 |
12 | class Meta:
13 | ordering = ('id',)
14 | verbose_name = 'despesa'
15 | verbose_name_plural = 'despesas'
16 |
17 | def __str__(self):
18 | return self.description
19 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/backend/core/templatetags/rows_tags.py:
--------------------------------------------------------------------------------
1 | from django import template
2 |
3 | register = template.Library()
4 |
5 |
6 | @register.simple_tag
7 | def first_line_count(*args, **kwargs):
8 | '''
9 | Calcula o primeiro valor do número de linhas.
10 | '''
11 | pg_number = kwargs.get('pg_number')
12 | rows_per_page = int(kwargs.get('rows_per_page'))
13 | return pg_number * rows_per_page - rows_per_page + 1
14 |
15 |
16 | @register.simple_tag
17 | def last_line_count(*args, **kwargs):
18 | '''
19 | Calcula o último valor do número de linhas.
20 | '''
21 | pg_number = kwargs.get('pg_number')
22 | rows_per_page = int(kwargs.get('rows_per_page'))
23 | total_items = kwargs.get('total_items')
24 |
25 | last_line_count = pg_number * rows_per_page
26 |
27 | if total_items < last_line_count:
28 | return total_items
29 | return last_line_count
30 |
--------------------------------------------------------------------------------
/backend/core/static/css/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin-top: 70px;
3 | }
4 |
5 | label.required:after {
6 | content: ' *';
7 | color: red;
8 | }
9 |
10 | .span-is-link {
11 | cursor: pointer;
12 | }
13 |
14 | .link {
15 | color: #007bff;
16 | }
17 |
18 | .ok {
19 | color: green;
20 | }
21 |
22 | .no {
23 | color: red;
24 | }
25 |
26 | tr.htmx-swapping td {
27 | opacity: 0;
28 | transition: opacity 0.5s ease-out;
29 | }
30 |
31 | .htmx-settling tr.deactivate td {
32 | background: lightcoral;
33 | }
34 |
35 | .htmx-settling tr.activate td {
36 | background: darkseagreen;
37 | }
38 |
39 | tr td {
40 | transition: all 1.2s;
41 | }
42 |
43 | .deactivate {
44 | text-decoration: line-through;
45 | }
46 |
47 | input[type=checkbox] {
48 | /* Double-sized Checkboxes */
49 | -ms-transform: scale(2);
50 | -moz-transform: scale(2);
51 | -webkit-transform: scale(2);
52 | -o-transform: scale(2);
53 | padding: 10px;
54 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/backend/core/templates/includes/nav.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Navbar
6 |
7 |
8 |
9 |
26 |
--------------------------------------------------------------------------------
/backend/expense/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.0 on 2021-12-25 06:05
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | initial = True
9 |
10 | dependencies = [
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='Expense',
16 | fields=[
17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18 | ('created', models.DateTimeField(auto_now_add=True, verbose_name='criado em')),
19 | ('modified', models.DateTimeField(auto_now=True, verbose_name='modificado em')),
20 | ('description', models.CharField(max_length=50, verbose_name='descrição')),
21 | ('payment_date', models.DateField(blank=True, null=True, verbose_name='data de pagamento')),
22 | ('value', models.DecimalField(decimal_places=2, max_digits=7, verbose_name='valor')),
23 | ('paid', models.BooleanField(default=False, verbose_name='pago')),
24 | ],
25 | options={
26 | 'verbose_name': 'despesa',
27 | 'verbose_name_plural': 'despesas',
28 | 'ordering': ('-payment_date',),
29 | },
30 | ),
31 | ]
32 |
--------------------------------------------------------------------------------
/backend/core/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 | {% load static %}
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Django
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | {% block css %}{% endblock css %}
26 |
27 |
28 |
29 |
30 |
31 | {% include "includes/nav.html" %}
32 | {% block content %}{% endblock content %}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | {% block js %}{% endblock js %}
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Django datatables htmx
2 |
3 | Layout based on https://www.cssscript.com/minimal-data-table/
4 |
5 | ## This project was done with:
6 |
7 | * [Python 3.9.6](https://www.python.org/)
8 | * [Django 4.0](https://www.djangoproject.com/)
9 |
10 | ## How to run project?
11 |
12 | * Clone this repository.
13 | * Create virtualenv with Python 3.
14 | * Active the virtualenv.
15 | * Install dependences.
16 | * Run the migrations.
17 |
18 | ```
19 | git clone https://github.com/rg3915/django-datatables-htmx.git
20 | cd django-datatables-htmx
21 | python -m venv .venv
22 | source .venv/bin/activate
23 | pip install -r requirements.txt
24 | python contrib/env_gen.py
25 | python manage.py migrate
26 | ```
27 |
28 | ## Django Seed
29 |
30 | if running [django-seed](https://github.com/Brobin/django-seed) type:
31 |
32 | ```
33 | python manage.py seed expense --number=145
34 | ```
35 |
36 |
37 | # Django datatables htmx
38 |
39 | Layout baseado em https://www.cssscript.com/minimal-data-table/
40 |
41 | ## Este projeto foi feito com:
42 |
43 | * [Python 3.9.6](https://www.python.org/)
44 | * [Django 4.0](https://www.djangoproject.com/)
45 |
46 | ## Como rodar o projeto?
47 |
48 | * Clone esse repositório.
49 | * Crie um virtualenv com Python 3.
50 | * Ative o virtualenv.
51 | * Instale as dependências.
52 | * Rode as migrações.
53 |
54 | ```
55 | git clone https://github.com/rg3915/django-datatables-htmx.git
56 | cd django-datatables-htmx
57 | python -m venv .venv
58 | source .venv/bin/activate
59 | pip install -r requirements.txt
60 | python contrib/env_gen.py
61 | python manage.py migrate
62 | ```
63 |
64 |
65 | ## Django Seed
66 |
67 | Se quiser rodar o [django-seed](https://github.com/Brobin/django-seed) digite:
68 |
69 | ```
70 | python manage.py seed expense --number=145
71 | ```
72 |
73 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/backend/core/templates/includes/pagination.html:
--------------------------------------------------------------------------------
1 |
2 | {% load url_replace %}
3 |
4 |
5 |
85 |
--------------------------------------------------------------------------------
/backend/expense/templates/expense/expense_list.html:
--------------------------------------------------------------------------------
1 |
2 | {% extends "base.html" %}
3 | {% load rows_tags %}
4 | {% load url_replace %}
5 |
6 | {% block content %}
7 |
8 |
9 |
10 |
11 |
19 | {% for row in rows_per_pages %}
20 |
21 |
27 | {{ row }}
28 |
29 | {% endfor %}
30 |
31 | Linhas por página
32 |
33 |
34 |
51 |
52 |
53 |
54 |
55 |
56 | ID
57 |
58 | {% include "../includes/ordering_field.html" with url='/expense/' field=sort_by.description target='#example' %}
59 | {% include "../includes/ordering_icon.html" with order=sort_by.description.ordering %}
60 |
61 |
62 | {% include "../includes/ordering_field.html" with url='/expense/' field=sort_by.value target='#example' %}
63 | {% include "../includes/ordering_icon.html" with order=sort_by.value.ordering %}
64 |
65 |
66 | {% include "../includes/ordering_field.html" with url='/expense/' field=sort_by.payment_date target='#example' %}
67 | {% include "../includes/ordering_icon.html" with order=sort_by.payment_date.ordering %}
68 |
69 |
70 |
71 |
72 | {% include "./expense_table.html" %}
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | Linhas {% first_line_count rows_per_page=rows_per_page pg_number=page_obj.number %}
81 | a {% last_line_count rows_per_page=rows_per_page pg_number=page_obj.number total_items=total_items %}
82 | de {{ page_obj.paginator.count }}
83 |
84 |
85 |
86 |
89 |
90 |
91 |
92 |
93 | {% endblock content %}
94 |
95 | {% block js %}
96 |
97 |
103 |
104 | {% endblock js %}
105 |
--------------------------------------------------------------------------------
/backend/expense/views.py:
--------------------------------------------------------------------------------
1 | from django.db.models import Q
2 | from django.views.generic import ListView
3 |
4 | from .models import Expense
5 |
6 |
7 | class DatatablesMixin:
8 | paginate = 10
9 | rows_per_pages = (5, 10, 20, 50, 100)
10 |
11 | def get_context_data(self, **kwargs):
12 | context = super().get_context_data(**kwargs)
13 | context['rows_per_pages'] = self.rows_per_pages
14 |
15 | # Busca
16 | search = self.request.GET.get('search')
17 | if search:
18 | context['search'] = search
19 |
20 | # Ordenação
21 | sort_by = self.request.GET.get('sort_by')
22 | if sort_by:
23 | if '-' in sort_by:
24 | _sort_by = sort_by.replace('-', '')
25 | # key é o nome do campo a ser ordenado.
26 | key = _sort_by
27 | else:
28 | # Muda a ordenação (como se fosse toggle).
29 | _sort_by = f'-{sort_by}'
30 | key = sort_by
31 | else:
32 | # Pega o nome do primeiro campo de ordenação do model.
33 | key = self.model._meta.ordering[0].replace('-', '')
34 | _sort_by = key
35 |
36 | # Atualiza o dicionário procurando pelo campo de ordenação.
37 | self.sort_by[key]['ordering'] = _sort_by
38 | context['sort_by'] = self.sort_by
39 | context['ordering'] = _sort_by
40 |
41 | # Linhas por página
42 | rows_per_page = self.request.GET.get('rows_per_page')
43 | if rows_per_page:
44 | context['rows_per_page'] = rows_per_page
45 | else:
46 | context['rows_per_page'] = self.paginate
47 |
48 | # Total de itens
49 | total_items = self.model.objects.values_list('id', flat=True).count()
50 | context['total_items'] = total_items
51 |
52 | return context
53 |
54 | def get_paginate_by(self, queryset):
55 | rows_per_page = self.request.GET.get('rows_per_page')
56 | if rows_per_page:
57 | return rows_per_page
58 | return self.paginate
59 |
60 |
61 | class ExpenseSearchMixin:
62 | '''
63 | Campo de busca
64 | '''
65 |
66 | def get_queryset(self):
67 | queryset = super().get_queryset()
68 | search = self.request.GET.get('search')
69 |
70 | if search:
71 | return queryset.filter(
72 | Q(description__icontains=search)
73 | )
74 | return queryset
75 |
76 |
77 | class ExpenseSortMixin:
78 | '''
79 | Ordenação
80 | '''
81 |
82 | def __init__(self):
83 | self.sort_by = {
84 | 'id': {
85 | 'label': self.model.id.field.verbose_name.capitalize(),
86 | 'ordering': self.model.id.field.name,
87 | },
88 | 'description': {
89 | 'label': self.model.description.field.verbose_name.capitalize(),
90 | 'ordering': self.model.description.field.name,
91 | },
92 | 'payment_date': {
93 | 'label': self.model.payment_date.field.verbose_name.capitalize(),
94 | 'ordering': self.model.payment_date.field.name,
95 | },
96 | 'value': {
97 | 'label': self.model.value.field.verbose_name.capitalize(),
98 | 'ordering': self.model.value.field.name,
99 | },
100 | }
101 |
102 | def get_context_data(self, **kwargs):
103 | context = super().get_context_data(**kwargs)
104 | context['sort_by'] = self.sort_by
105 | return context
106 |
107 | def get_queryset(self):
108 | queryset = super().get_queryset()
109 | sort_by = self.request.GET.get('sort_by')
110 |
111 | if sort_by:
112 | return queryset.order_by(sort_by)
113 | return queryset
114 |
115 |
116 | class ExpenseListView(DatatablesMixin, ExpenseSearchMixin, ExpenseSortMixin, ListView):
117 | model = Expense
118 | template_name = 'expense/expense_list.html'
119 |
--------------------------------------------------------------------------------
/backend/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for backend project.
3 |
4 | Generated by 'django-admin startproject' using Django 4.0.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.0/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/4.0/ref/settings/
11 | """
12 |
13 | from pathlib import Path
14 |
15 | from decouple import Csv, config
16 | from dj_database_url import parse as dburl
17 |
18 | # Build paths inside the project like this: BASE_DIR / 'subdir'.
19 | BASE_DIR = Path(__file__).resolve().parent.parent
20 |
21 |
22 | # Quick-start development settings - unsuitable for production
23 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
24 |
25 | # SECURITY WARNING: keep the secret key used in production secret!
26 | SECRET_KEY = config('SECRET_KEY')
27 |
28 | # SECURITY WARNING: don't run with debug turned on in production!
29 | DEBUG = config('DEBUG', default=False, cast=bool)
30 |
31 | ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=[], cast=Csv())
32 |
33 |
34 | # Application definition
35 |
36 | INSTALLED_APPS = [
37 | 'django.contrib.admin',
38 | 'django.contrib.auth',
39 | 'django.contrib.contenttypes',
40 | 'django.contrib.sessions',
41 | 'django.contrib.messages',
42 | 'django.contrib.staticfiles',
43 | # 3rd apps
44 | 'django_extensions',
45 | 'widget_tweaks',
46 | 'django_seed',
47 | # my apps
48 | 'backend.core.apps.CoreConfig',
49 | 'backend.expense.apps.ExpenseConfig',
50 | ]
51 |
52 | MIDDLEWARE = [
53 | 'django.middleware.security.SecurityMiddleware',
54 | 'django.contrib.sessions.middleware.SessionMiddleware',
55 | 'django.middleware.common.CommonMiddleware',
56 | 'django.middleware.csrf.CsrfViewMiddleware',
57 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
58 | 'django.contrib.messages.middleware.MessageMiddleware',
59 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
60 | ]
61 |
62 | ROOT_URLCONF = 'backend.urls'
63 |
64 | TEMPLATES = [
65 | {
66 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
67 | 'DIRS': [],
68 | 'APP_DIRS': True,
69 | 'OPTIONS': {
70 | 'context_processors': [
71 | 'django.template.context_processors.debug',
72 | 'django.template.context_processors.request',
73 | 'django.contrib.auth.context_processors.auth',
74 | 'django.contrib.messages.context_processors.messages',
75 | ],
76 | },
77 | },
78 | ]
79 |
80 | WSGI_APPLICATION = 'backend.wsgi.application'
81 |
82 |
83 | # Database
84 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases
85 |
86 | default_dburl = 'sqlite:///' + str(BASE_DIR / 'db.sqlite3')
87 | DATABASES = {
88 | 'default': config('DATABASE_URL', default=default_dburl, cast=dburl),
89 | }
90 |
91 |
92 | # Password validation
93 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
94 |
95 | AUTH_PASSWORD_VALIDATORS = [
96 | {
97 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
98 | },
99 | {
100 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
101 | },
102 | {
103 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
104 | },
105 | {
106 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
107 | },
108 | ]
109 |
110 |
111 | # Internationalization
112 | # https://docs.djangoproject.com/en/4.0/topics/i18n/
113 |
114 | LANGUAGE_CODE = 'pt-br' # 'en-us'
115 |
116 | TIME_ZONE = 'America/Sao_Paulo' # 'UTC'
117 |
118 | USE_I18N = True
119 |
120 | USE_L10N = True
121 |
122 | USE_TZ = True
123 |
124 | USE_THOUSAND_SEPARATOR = True
125 |
126 | DECIMAL_SEPARATOR = ','
127 |
128 |
129 | # Static files (CSS, JavaScript, Images)
130 | # https://docs.djangoproject.com/en/4.0/howto/static-files/
131 |
132 | STATIC_URL = '/static/'
133 | STATIC_ROOT = BASE_DIR.joinpath('staticfiles')
134 |
135 | # Default primary key field type
136 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
137 |
138 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
139 |
140 |
141 | LOGIN_URL = '/admin/login/'
142 | LOGIN_REDIRECT_URL = 'core:index'
143 | # LOGOUT_REDIRECT_URL = 'core:index'
144 |
--------------------------------------------------------------------------------
/backend/core/static/css/icons/simple-line-icons.min.css:
--------------------------------------------------------------------------------
1 | @font-face{font-family:simple-line-icons;src:url(../../fonts/Simple-Line-Icons.eot);src:url(../../fonts/Simple-Line-Icons.eot) format('embedded-opentype'),url(../../fonts/Simple-Line-Icons.woff2) format('woff2'),url(../../fonts/Simple-Line-Icons.ttf) format('truetype'),url(../../fonts/Simple-Line-Icons.woff) format('woff'),url(../../fonts/Simple-Line-Icons.svg#simple-line-icons) format('svg');font-weight:400;font-style:normal}.icon-action-redo,.icon-action-undo,.icon-anchor,.icon-arrow-down,.icon-arrow-down-circle,.icon-arrow-left,.icon-arrow-left-circle,.icon-arrow-right,.icon-arrow-right-circle,.icon-arrow-up,.icon-arrow-up-circle,.icon-badge,.icon-bag,.icon-ban,.icon-basket,.icon-basket-loaded,.icon-bell,.icon-book-open,.icon-briefcase,.icon-bubble,.icon-bubbles,.icon-bulb,.icon-calculator,.icon-calendar,.icon-call-end,.icon-call-in,.icon-call-out,.icon-camera,.icon-camrecorder,.icon-chart,.icon-check,.icon-chemistry,.icon-clock,.icon-close,.icon-cloud-download,.icon-cloud-upload,.icon-compass,.icon-control-end,.icon-control-forward,.icon-control-pause,.icon-control-play,.icon-control-rewind,.icon-control-start,.icon-credit-card,.icon-crop,.icon-cup,.icon-cursor,.icon-cursor-move,.icon-diamond,.icon-direction,.icon-directions,.icon-disc,.icon-dislike,.icon-doc,.icon-docs,.icon-drawer,.icon-drop,.icon-earphones,.icon-earphones-alt,.icon-emotsmile,.icon-energy,.icon-envelope,.icon-envelope-letter,.icon-envelope-open,.icon-equalizer,.icon-event,.icon-exclamation,.icon-eye,.icon-eyeglass,.icon-feed,.icon-film,.icon-fire,.icon-flag,.icon-folder,.icon-folder-alt,.icon-frame,.icon-game-controller,.icon-ghost,.icon-globe,.icon-globe-alt,.icon-graduation,.icon-graph,.icon-grid,.icon-handbag,.icon-heart,.icon-home,.icon-hourglass,.icon-info,.icon-key,.icon-layers,.icon-like,.icon-link,.icon-list,.icon-location-pin,.icon-lock,.icon-lock-open,.icon-login,.icon-logout,.icon-loop,.icon-magic-wand,.icon-magnet,.icon-magnifier,.icon-magnifier-add,.icon-magnifier-remove,.icon-map,.icon-menu,.icon-microphone,.icon-minus,.icon-mouse,.icon-music-tone,.icon-music-tone-alt,.icon-mustache,.icon-note,.icon-notebook,.icon-options,.icon-options-vertical,.icon-organization,.icon-paper-clip,.icon-paper-plane,.icon-paypal,.icon-pencil,.icon-people,.icon-phone,.icon-picture,.icon-pie-chart,.icon-pin,.icon-plane,.icon-playlist,.icon-plus,.icon-power,.icon-present,.icon-printer,.icon-puzzle,.icon-question,.icon-refresh,.icon-reload,.icon-rocket,.icon-screen-desktop,.icon-screen-smartphone,.icon-screen-tablet,.icon-settings,.icon-share,.icon-share-alt,.icon-shield,.icon-shuffle,.icon-size-actual,.icon-size-fullscreen,.icon-social-behance,.icon-social-dribbble,.icon-social-dropbox,.icon-social-facebook,.icon-social-foursqare,.icon-social-github,.icon-social-google,.icon-social-instagram,.icon-social-linkedin,.icon-social-pinterest,.icon-social-reddit,.icon-social-skype,.icon-social-soundcloud,.icon-social-spotify,.icon-social-steam,.icon-social-stumbleupon,.icon-social-tumblr,.icon-social-twitter,.icon-social-vkontakte,.icon-social-youtube,.icon-speech,.icon-speedometer,.icon-star,.icon-support,.icon-symbol-female,.icon-symbol-male,.icon-tag,.icon-target,.icon-trash,.icon-trophy,.icon-umbrella,.icon-user,.icon-user-female,.icon-user-follow,.icon-user-following,.icon-user-unfollow,.icon-vector,.icon-volume-1,.icon-volume-2,.icon-volume-off,.icon-wallet,.icon-wrench{font-family:simple-line-icons;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-user:before{content:"\e005"}.icon-people:before{content:"\e001"}.icon-user-female:before{content:"\e000"}.icon-user-follow:before{content:"\e002"}.icon-user-following:before{content:"\e003"}.icon-user-unfollow:before{content:"\e004"}.icon-login:before{content:"\e066"}.icon-logout:before{content:"\e065"}.icon-emotsmile:before{content:"\e021"}.icon-phone:before{content:"\e600"}.icon-call-end:before{content:"\e048"}.icon-call-in:before{content:"\e047"}.icon-call-out:before{content:"\e046"}.icon-map:before{content:"\e033"}.icon-location-pin:before{content:"\e096"}.icon-direction:before{content:"\e042"}.icon-directions:before{content:"\e041"}.icon-compass:before{content:"\e045"}.icon-layers:before{content:"\e034"}.icon-menu:before{content:"\e601"}.icon-list:before{content:"\e067"}.icon-options-vertical:before{content:"\e602"}.icon-options:before{content:"\e603"}.icon-arrow-down:before{content:"\e604"}.icon-arrow-left:before{content:"\e605"}.icon-arrow-right:before{content:"\e606"}.icon-arrow-up:before{content:"\e607"}.icon-arrow-up-circle:before{content:"\e078"}.icon-arrow-left-circle:before{content:"\e07a"}.icon-arrow-right-circle:before{content:"\e079"}.icon-arrow-down-circle:before{content:"\e07b"}.icon-check:before{content:"\e080"}.icon-clock:before{content:"\e081"}.icon-plus:before{content:"\e095"}.icon-minus:before{content:"\e615"}.icon-close:before{content:"\e082"}.icon-event:before{content:"\e619"}.icon-exclamation:before{content:"\e617"}.icon-organization:before{content:"\e616"}.icon-trophy:before{content:"\e006"}.icon-screen-smartphone:before{content:"\e010"}.icon-screen-desktop:before{content:"\e011"}.icon-plane:before{content:"\e012"}.icon-notebook:before{content:"\e013"}.icon-mustache:before{content:"\e014"}.icon-mouse:before{content:"\e015"}.icon-magnet:before{content:"\e016"}.icon-energy:before{content:"\e020"}.icon-disc:before{content:"\e022"}.icon-cursor:before{content:"\e06e"}.icon-cursor-move:before{content:"\e023"}.icon-crop:before{content:"\e024"}.icon-chemistry:before{content:"\e026"}.icon-speedometer:before{content:"\e007"}.icon-shield:before{content:"\e00e"}.icon-screen-tablet:before{content:"\e00f"}.icon-magic-wand:before{content:"\e017"}.icon-hourglass:before{content:"\e018"}.icon-graduation:before{content:"\e019"}.icon-ghost:before{content:"\e01a"}.icon-game-controller:before{content:"\e01b"}.icon-fire:before{content:"\e01c"}.icon-eyeglass:before{content:"\e01d"}.icon-envelope-open:before{content:"\e01e"}.icon-envelope-letter:before{content:"\e01f"}.icon-bell:before{content:"\e027"}.icon-badge:before{content:"\e028"}.icon-anchor:before{content:"\e029"}.icon-wallet:before{content:"\e02a"}.icon-vector:before{content:"\e02b"}.icon-speech:before{content:"\e02c"}.icon-puzzle:before{content:"\e02d"}.icon-printer:before{content:"\e02e"}.icon-present:before{content:"\e02f"}.icon-playlist:before{content:"\e030"}.icon-pin:before{content:"\e031"}.icon-picture:before{content:"\e032"}.icon-handbag:before{content:"\e035"}.icon-globe-alt:before{content:"\e036"}.icon-globe:before{content:"\e037"}.icon-folder-alt:before{content:"\e039"}.icon-folder:before{content:"\e089"}.icon-film:before{content:"\e03a"}.icon-feed:before{content:"\e03b"}.icon-drop:before{content:"\e03e"}.icon-drawer:before{content:"\e03f"}.icon-docs:before{content:"\e040"}.icon-doc:before{content:"\e085"}.icon-diamond:before{content:"\e043"}.icon-cup:before{content:"\e044"}.icon-calculator:before{content:"\e049"}.icon-bubbles:before{content:"\e04a"}.icon-briefcase:before{content:"\e04b"}.icon-book-open:before{content:"\e04c"}.icon-basket-loaded:before{content:"\e04d"}.icon-basket:before{content:"\e04e"}.icon-bag:before{content:"\e04f"}.icon-action-undo:before{content:"\e050"}.icon-action-redo:before{content:"\e051"}.icon-wrench:before{content:"\e052"}.icon-umbrella:before{content:"\e053"}.icon-trash:before{content:"\e054"}.icon-tag:before{content:"\e055"}.icon-support:before{content:"\e056"}.icon-frame:before{content:"\e038"}.icon-size-fullscreen:before{content:"\e057"}.icon-size-actual:before{content:"\e058"}.icon-shuffle:before{content:"\e059"}.icon-share-alt:before{content:"\e05a"}.icon-share:before{content:"\e05b"}.icon-rocket:before{content:"\e05c"}.icon-question:before{content:"\e05d"}.icon-pie-chart:before{content:"\e05e"}.icon-pencil:before{content:"\e05f"}.icon-note:before{content:"\e060"}.icon-loop:before{content:"\e064"}.icon-home:before{content:"\e069"}.icon-grid:before{content:"\e06a"}.icon-graph:before{content:"\e06b"}.icon-microphone:before{content:"\e063"}.icon-music-tone-alt:before{content:"\e061"}.icon-music-tone:before{content:"\e062"}.icon-earphones-alt:before{content:"\e03c"}.icon-earphones:before{content:"\e03d"}.icon-equalizer:before{content:"\e06c"}.icon-like:before{content:"\e068"}.icon-dislike:before{content:"\e06d"}.icon-control-start:before{content:"\e06f"}.icon-control-rewind:before{content:"\e070"}.icon-control-play:before{content:"\e071"}.icon-control-pause:before{content:"\e072"}.icon-control-forward:before{content:"\e073"}.icon-control-end:before{content:"\e074"}.icon-volume-1:before{content:"\e09f"}.icon-volume-2:before{content:"\e0a0"}.icon-volume-off:before{content:"\e0a1"}.icon-calendar:before{content:"\e075"}.icon-bulb:before{content:"\e076"}.icon-chart:before{content:"\e077"}.icon-ban:before{content:"\e07c"}.icon-bubble:before{content:"\e07d"}.icon-camrecorder:before{content:"\e07e"}.icon-camera:before{content:"\e07f"}.icon-cloud-download:before{content:"\e083"}.icon-cloud-upload:before{content:"\e084"}.icon-envelope:before{content:"\e086"}.icon-eye:before{content:"\e087"}.icon-flag:before{content:"\e088"}.icon-heart:before{content:"\e08a"}.icon-info:before{content:"\e08b"}.icon-key:before{content:"\e08c"}.icon-link:before{content:"\e08d"}.icon-lock:before{content:"\e08e"}.icon-lock-open:before{content:"\e08f"}.icon-magnifier:before{content:"\e090"}.icon-magnifier-add:before{content:"\e091"}.icon-magnifier-remove:before{content:"\e092"}.icon-paper-clip:before{content:"\e093"}.icon-paper-plane:before{content:"\e094"}.icon-power:before{content:"\e097"}.icon-refresh:before{content:"\e098"}.icon-reload:before{content:"\e099"}.icon-settings:before{content:"\e09a"}.icon-star:before{content:"\e09b"}.icon-symbol-female:before{content:"\e09c"}.icon-symbol-male:before{content:"\e09d"}.icon-target:before{content:"\e09e"}.icon-credit-card:before{content:"\e025"}.icon-paypal:before{content:"\e608"}.icon-social-tumblr:before{content:"\e00a"}.icon-social-twitter:before{content:"\e009"}.icon-social-facebook:before{content:"\e00b"}.icon-social-instagram:before{content:"\e609"}.icon-social-linkedin:before{content:"\e60a"}.icon-social-pinterest:before{content:"\e60b"}.icon-social-github:before{content:"\e60c"}.icon-social-google:before{content:"\e60d"}.icon-social-reddit:before{content:"\e60e"}.icon-social-skype:before{content:"\e60f"}.icon-social-dribbble:before{content:"\e00d"}.icon-social-behance:before{content:"\e610"}.icon-social-foursqare:before{content:"\e611"}.icon-social-soundcloud:before{content:"\e612"}.icon-social-spotify:before{content:"\e613"}.icon-social-stumbleupon:before{content:"\e614"}.icon-social-youtube:before{content:"\e008"}.icon-social-dropbox:before{content:"\e00c"}.icon-social-vkontakte:before{content:"\e618"}.icon-social-steam:before{content:"\e620"}/*# sourceMappingURL=simple-line-icons.min.css.map */
--------------------------------------------------------------------------------