├── wildewidgets ├── settings.py ├── templatetags │ ├── __init__.py │ └── wildewidgets.py ├── admin.py ├── templates │ └── wildewidgets │ │ ├── list_model_widget.html │ │ ├── html_widget.html │ │ ├── markdown_widget.html │ │ ├── link_button.html │ │ ├── crispy_form_modal.html │ │ ├── crispy_form_widget.html │ │ ├── widget_stream.html │ │ ├── header_with_widget.html │ │ ├── header_with_link_button.html │ │ ├── header_with_modal_button.html │ │ ├── header_with_collapse_button.html │ │ ├── button--form.html │ │ ├── widget-list.html │ │ ├── widget_index.html │ │ ├── apex_json.html │ │ ├── initials_avatar.html │ │ ├── apex_chart.html │ │ ├── header_with_controls.html │ │ ├── block--simple.html │ │ ├── widget-list--main.html │ │ ├── widget-list--sidebar.html │ │ ├── block.html │ │ ├── card_block.html │ │ ├── altairchart.html │ │ ├── modal.html │ │ ├── page_tab_block.html │ │ ├── tab_block.html │ │ ├── doughnutchart_json.html │ │ ├── paged_model_widget.html │ │ ├── doughnutchart.html │ │ ├── stackedbarchart_json.html │ │ ├── stackedbarchart.html │ │ ├── barchart.html │ │ ├── menu.html │ │ └── categorychart.html ├── tests.py ├── static │ └── wildewidgets │ │ ├── css │ │ ├── _widget-list.scss │ │ ├── wildewidgets.scss │ │ ├── _widget-index.scss │ │ ├── table_extra.css │ │ ├── _toggleablemanytomanyfieldblock.scss │ │ ├── _navbar.scss │ │ └── highlighting.css │ │ ├── images │ │ └── placeholder.png │ │ └── js │ │ └── wildewidgets.js ├── apps.py ├── widgets │ ├── charts │ │ ├── __init__.py │ │ └── altair.py │ ├── tables │ │ ├── __init__.py │ │ └── components.py │ ├── __init__.py │ ├── icons.py │ └── modals.py ├── views │ ├── __init__.py │ ├── tables.py │ └── json.py └── __init__.py ├── demo ├── demo │ ├── core │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ ├── 0001_initial.py │ │ │ └── 0002_initial_data.py │ │ ├── templatetags │ │ │ └── __init__.py │ │ ├── __init__.py │ │ ├── tests.py │ │ ├── forms.py │ │ ├── static │ │ │ └── core │ │ │ │ └── images │ │ │ │ └── dark_logo.png │ │ ├── apps.py │ │ ├── jobs.py │ │ ├── admin.py │ │ ├── models.py │ │ ├── urls.py │ │ ├── templates │ │ │ └── core │ │ │ │ └── intermediate.html │ │ ├── fixtures │ │ │ └── users.json │ │ └── views.py │ ├── users │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ ├── 0002_load_fixture.py │ │ │ └── 0001_initial.py │ │ ├── __init__.py │ │ ├── tests.py │ │ ├── apps.py │ │ ├── models.py │ │ ├── templates │ │ │ ├── users │ │ │ │ └── intermediate.html │ │ │ └── registration │ │ │ │ └── login.html │ │ └── fixtures │ │ │ └── users.json │ ├── __init__.py │ ├── urls.py │ ├── settings_docker.py │ ├── gunicorn_config.py │ └── wsgi.py ├── .dockerignore ├── bin │ ├── restart-gunicorn.sh │ ├── collectstatic.sh │ └── wait-for-it.sh ├── etc │ ├── ipython_config.py │ ├── gunicorn_logging.conf │ ├── supervisord.conf │ ├── environment.txt │ └── nginx.conf ├── sql │ └── docker │ │ ├── init.sql │ │ └── my.cnf ├── setup.py ├── setup.cfg ├── manage.py ├── docker-compose.yml ├── Makefile ├── .gitignore ├── Dockerfile ├── README.md └── requirements.txt ├── .autoenv ├── docs ├── favicon.ico ├── demo_ss_home.png ├── _static │ ├── favicon.ico │ ├── wildewidgets.png │ ├── wildewidgets_logo.png │ ├── wildewidgets_logo.pxd │ ├── wildewidgets_dark_mode_logo.png │ └── wildewidgets_dark_mode_logo.pxd ├── demo_ss_simple_table.png ├── api_icons.rst ├── api_base.rst ├── api_grid.rst ├── api_misc.rst ├── api_text.rst ├── api_layout.rst ├── api_modals.rst ├── api_buttons.rst ├── api_headers.rst ├── api_datagrid.rst ├── api_forms.rst ├── api_structure.rst ├── charts.rst ├── guide.rst ├── api_navigation.rst ├── api.rst ├── requirements.txt ├── api_charts.rst ├── api_tables.rst ├── Makefile ├── make.bat ├── index.rst ├── scientific_charts.rst ├── api_views.rst ├── widgets.rst ├── install.rst ├── conf.py └── business_charts.rst ├── .autoenv.leave ├── MANIFEST.in ├── .readthedocs.yaml ├── .bumpversion.cfg ├── Makefile ├── bin └── release.sh ├── LICENSE.txt ├── .gitignore ├── README.md └── requirements.txt /wildewidgets/settings.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/demo/core/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/demo/users/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wildewidgets/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/demo/core/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/demo/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.2.4" 2 | -------------------------------------------------------------------------------- /wildewidgets/admin.py: -------------------------------------------------------------------------------- 1 | # Register your models here. 2 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/list_model_widget.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .idea 3 | 4 | 5 | /*.sql 6 | 7 | tags 8 | -------------------------------------------------------------------------------- /demo/bin/restart-gunicorn.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | killall -HUP gunicorn 3 | -------------------------------------------------------------------------------- /demo/demo/core/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'demo.core.apps.CoreConfig' 2 | -------------------------------------------------------------------------------- /demo/demo/users/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'demo.users.apps.UsersConfig' 2 | -------------------------------------------------------------------------------- /.autoenv: -------------------------------------------------------------------------------- 1 | if [[ -f .venv/bin/activate ]]; then 2 | source .venv/bin/activate 3 | fi 4 | -------------------------------------------------------------------------------- /wildewidgets/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /demo/demo/core/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /demo/demo/users/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /.autoenv.leave: -------------------------------------------------------------------------------- 1 | CWD=$(pwd) 2 | if [[ ! $CWD == *"$VIRTUAL_ENV_PROMPT"* ]]; then 3 | deactivate 4 | fi 5 | -------------------------------------------------------------------------------- /docs/demo_ss_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/HEAD/docs/demo_ss_home.png -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/HEAD/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/_static/wildewidgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/HEAD/docs/_static/wildewidgets.png -------------------------------------------------------------------------------- /docs/demo_ss_simple_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/HEAD/docs/demo_ss_simple_table.png -------------------------------------------------------------------------------- /docs/api_icons.rst: -------------------------------------------------------------------------------- 1 | Icons 2 | ===== 3 | 4 | 5 | .. automodule:: wildewidgets.widgets.icons 6 | :members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/_static/wildewidgets_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/HEAD/docs/_static/wildewidgets_logo.png -------------------------------------------------------------------------------- /docs/_static/wildewidgets_logo.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/HEAD/docs/_static/wildewidgets_logo.pxd -------------------------------------------------------------------------------- /wildewidgets/static/wildewidgets/css/_widget-list.scss: -------------------------------------------------------------------------------- 1 | .widget-list { 2 | &__sidebar { 3 | font-size: 1.2rem; 4 | } 5 | } 6 | 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include README.md 3 | recursive-include wildewidgets/static * 4 | recursive-include wildewidgets/templates * 5 | -------------------------------------------------------------------------------- /demo/demo/core/forms.py: -------------------------------------------------------------------------------- 1 | #################################### 2 | # Define your core app's forms here. 3 | #################################### 4 | -------------------------------------------------------------------------------- /docs/api_base.rst: -------------------------------------------------------------------------------- 1 | Base Widgets 2 | ============ 3 | 4 | 5 | .. automodule:: wildewidgets.widgets.base 6 | :members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/api_grid.rst: -------------------------------------------------------------------------------- 1 | Boostrap Grid 2 | ============= 3 | 4 | 5 | .. automodule:: wildewidgets.widgets.grid 6 | :members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/api_misc.rst: -------------------------------------------------------------------------------- 1 | Misc Widgets 2 | ============ 3 | 4 | 5 | .. automodule:: wildewidgets.widgets.misc 6 | :members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/api_text.rst: -------------------------------------------------------------------------------- 1 | Text Widgets 2 | ============ 3 | 4 | 5 | .. automodule:: wildewidgets.widgets.text 6 | :members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/api_layout.rst: -------------------------------------------------------------------------------- 1 | Layout Widget 2 | ============= 3 | 4 | 5 | .. automodule:: wildewidgets.widgets.layout 6 | :members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/api_modals.rst: -------------------------------------------------------------------------------- 1 | Modal Widgets 2 | ============= 3 | 4 | 5 | .. automodule:: wildewidgets.widgets.modals 6 | :members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /demo/demo/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | name = 'demo.users' 6 | label = 'users' 7 | -------------------------------------------------------------------------------- /docs/_static/wildewidgets_dark_mode_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/HEAD/docs/_static/wildewidgets_dark_mode_logo.png -------------------------------------------------------------------------------- /docs/_static/wildewidgets_dark_mode_logo.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/HEAD/docs/_static/wildewidgets_dark_mode_logo.pxd -------------------------------------------------------------------------------- /docs/api_buttons.rst: -------------------------------------------------------------------------------- 1 | Button Widgets 2 | ============== 3 | 4 | 5 | .. automodule:: wildewidgets.widgets.buttons 6 | :members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/api_headers.rst: -------------------------------------------------------------------------------- 1 | Header Widgets 2 | ============== 3 | 4 | 5 | .. automodule:: wildewidgets.widgets.headers 6 | :members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /wildewidgets/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class WildewidgetsConfig(AppConfig): # noqa: D101 5 | name: str = "wildewidgets" 6 | -------------------------------------------------------------------------------- /wildewidgets/widgets/charts/__init__.py: -------------------------------------------------------------------------------- 1 | from .altair import * # noqa: F403 2 | from .apex import * # noqa: F403 3 | from .chartjs import * # noqa: F403 4 | -------------------------------------------------------------------------------- /demo/demo/core/static/core/images/dark_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/HEAD/demo/demo/core/static/core/images/dark_logo.png -------------------------------------------------------------------------------- /docs/api_datagrid.rst: -------------------------------------------------------------------------------- 1 | Tabler Datagrid 2 | =============== 3 | 4 | 5 | .. automodule:: wildewidgets.widgets.datagrid 6 | :members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/api_forms.rst: -------------------------------------------------------------------------------- 1 | Form related widgets 2 | ==================== 3 | 4 | 5 | .. automodule:: wildewidgets.widgets.forms 6 | :members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /demo/demo/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): # noqa: D101 5 | name = "demo.core" 6 | label = "core" 7 | -------------------------------------------------------------------------------- /docs/api_structure.rst: -------------------------------------------------------------------------------- 1 | Structure Widgets 2 | ================= 3 | 4 | 5 | .. automodule:: wildewidgets.widgets.structure 6 | :members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /wildewidgets/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .json import * # noqa: F403,F401 2 | from .mixins import * # noqa: F403,F401 3 | from .tables import * # noqa: F403,F401 4 | 5 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/html_widget.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{html|safe}} 4 |
-------------------------------------------------------------------------------- /wildewidgets/static/wildewidgets/images/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/HEAD/wildewidgets/static/wildewidgets/images/placeholder.png -------------------------------------------------------------------------------- /docs/charts.rst: -------------------------------------------------------------------------------- 1 | Charts 2 | ------ 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | business_charts 9 | scientific_charts 10 | 11 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/markdown_widget.html: -------------------------------------------------------------------------------- 1 | {% load markdownify %} 2 | 3 |
4 | {{text|markdownify}} 5 |
-------------------------------------------------------------------------------- /docs/guide.rst: -------------------------------------------------------------------------------- 1 | User Guide 2 | ========== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | install 9 | charts 10 | tables 11 | widgets 12 | 13 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/link_button.html: -------------------------------------------------------------------------------- 1 |
2 | {{ text }} 3 |
4 | -------------------------------------------------------------------------------- /wildewidgets/static/wildewidgets/css/wildewidgets.scss: -------------------------------------------------------------------------------- 1 | @import '_navbar.scss'; 2 | @import '_toggleablemanytomanyfieldblock.scss'; 3 | @import '_widget-index.scss'; 4 | @import '_widget-list.scss'; 5 | -------------------------------------------------------------------------------- /wildewidgets/widgets/tables/__init__.py: -------------------------------------------------------------------------------- 1 | from .actions import * # noqa: F403 2 | from .components import * # noqa: F403 3 | from .tables import * # noqa: F403 4 | from .views import * # noqa: F403 5 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/crispy_form_modal.html: -------------------------------------------------------------------------------- 1 | {% extends 'wildewidgets/modal.html' %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block modal_body %} 5 | {% crispy form %} 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/crispy_form_widget.html: -------------------------------------------------------------------------------- 1 | {% extends 'wildewidgets/block.html' %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block block_content %} 5 | {% crispy form %} 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /demo/bin/collectstatic.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This script exists to simplify the repeated task of re-running collectstatic while developing css/js. 3 | python /demo/manage.py collectstatic --no-input -v0 --link 4 | -------------------------------------------------------------------------------- /demo/demo/users/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | 3 | 4 | class User(AbstractUser): 5 | 6 | class Meta: 7 | verbose_name = 'user' 8 | verbose_name_plural = 'users' 9 | -------------------------------------------------------------------------------- /wildewidgets/static/wildewidgets/css/_widget-index.scss: -------------------------------------------------------------------------------- 1 | .widget-index { 2 | &__item { 3 | font-size: 1.13rem; 4 | a, a:visited {color: black !important;} 5 | a:hover {text-decoration: underline;} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /docs/api_navigation.rst: -------------------------------------------------------------------------------- 1 | Navigation 2 | ========== 3 | 4 | .. automodule:: wildewidgets.menus 5 | :members: 6 | :show-inheritance: 7 | 8 | .. automodule:: wildewidgets.widgets.navigation 9 | :members: 10 | :show-inheritance: -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/widget_stream.html: -------------------------------------------------------------------------------- 1 | {% extends 'wildewidgets/block.html' %} 2 | {% load wildewidgets %} 3 | 4 | {% block block_content %} 5 | {% for item in widgets %} 6 | {% wildewidgets item %} 7 | {% endfor %} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/header_with_widget.html: -------------------------------------------------------------------------------- 1 | {% extends 'wildewidgets/header_with_controls.html' %} 2 | {% load wildewidgets %} 3 | 4 | {% block controls %} 5 | {% if widget %} 6 | {% wildewidgets widget %} 7 | {% endif %} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /demo/etc/ipython_config.py: -------------------------------------------------------------------------------- 1 | print("--------->>>>>>>> ENABLE AUTORELOAD <<<<<<<<<------------") 2 | 3 | c = get_config() 4 | c.InteractiveShellApp.exec_lines = [] 5 | c.InteractiveShellApp.exec_lines.append('%load_ext autoreload') 6 | c.InteractiveShellApp.exec_lines.append('%autoreload 2') 7 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/header_with_link_button.html: -------------------------------------------------------------------------------- 1 | {% extends 'wildewidgets/header_with_controls.html' %} 2 | 3 | {% block controls %} 4 |
5 | {{ link_text }} 6 |
7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /demo/demo/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from wildewidgets import WildewidgetDispatch 3 | 4 | from .core import urls as core_urls 5 | 6 | urlpatterns = [ 7 | path("wildewidgets_json", WildewidgetDispatch.as_view(), name="wildewidgets_json"), 8 | path("", include(core_urls, namespace="core")), 9 | ] 10 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/header_with_modal_button.html: -------------------------------------------------------------------------------- 1 | {% extends 'wildewidgets/header_with_controls.html' %} 2 | 3 | {% block controls %} 4 |
5 | 6 |
7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /wildewidgets/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.2.4" 2 | 3 | from .forms import * # noqa: F403 4 | from .menus import * # noqa: F403 5 | from .views import * # noqa: F403 6 | from .widgets import * # noqa: F403 7 | # WARNING: do not import .viewsets here, because it required Django to be 8 | # fully initialized. That makes it impossible to build the docs. 9 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/header_with_collapse_button.html: -------------------------------------------------------------------------------- 1 | {% extends 'wildewidgets/header_with_controls.html' %} 2 | 3 | {% block controls %} 4 |
5 | 6 |
7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/button--form.html: -------------------------------------------------------------------------------- 1 | {% extends 'wildewidgets/block.html' %} 2 | {% load wildewidgets %} 3 | 4 | {% block block_content %} 5 | {% csrf_token %} 6 | {% for name, value in data.items %} 7 | 8 | {% endfor %} 9 | {% wildewidgets button %} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /demo/sql/docker/init.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE demo CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; 2 | CREATE USER 'demo_u' IDENTIFIED WITH mysql_native_password BY 'password'; 3 | GRANT ALL PRIVILEGES ON demo.* TO 'demo_u'; 4 | -- Need to grant access to test database even though we don't create it until running the tests 5 | GRANT ALL PRIVILEGES ON test_demo.* TO 'demo_u'; 6 | -------------------------------------------------------------------------------- /demo/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup # noqa: INP001 2 | 3 | setup( 4 | name="demo", 5 | version="1.2.4", 6 | description="", 7 | author="Caltech IMSS ADS", 8 | author_email="imss-ads-staff@caltech.edu", 9 | packages=find_packages( 10 | exclude=["*.tests", "*.tests.*", "tests.*", "tests", "htmlcov"] 11 | ), 12 | ) 13 | -------------------------------------------------------------------------------- /demo/demo/core/jobs.py: -------------------------------------------------------------------------------- 1 | ############################################################################################################ 2 | # Define your core app's jobs here. 3 | # Jobs are generally functions that are called by management commands and/or 4 | # user-facing views. 5 | ############################################################################################################ 6 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/widget-list.html: -------------------------------------------------------------------------------- 1 | {% extends 'wildewidgets/block.html' %} 2 | {% load wildewidgets %} 3 | 4 | {% block block_content %} 5 | {% wildewidgets header %} 6 |
7 | {% wildewidgets sidebar %} 8 | {% wildewidgets main %} 9 |
10 | {% for modal in modals %} 11 | {% wildewidgets modal %} 12 | {% endfor %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | api_base 8 | api_buttons 9 | api_charts 10 | api_datagrid 11 | api_forms 12 | api_grid 13 | api_headers 14 | api_icons 15 | api_layout 16 | api_misc 17 | api_modals 18 | api_navigation 19 | api_structure 20 | api_tables 21 | api_text 22 | api_views 23 | 24 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/widget_index.html: -------------------------------------------------------------------------------- 1 | {% extends 'wildewidgets/block.html' %} 2 | 3 | {% block block_content %} 4 | {% for item in entries %} 5 |
6 | {{item.title}} 7 |
8 | {% endfor %} 9 | {% endblock %} 10 | 11 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/apex_json.html: -------------------------------------------------------------------------------- 1 | {% extends "wildewidgets/apex_chart.html" %} 2 | 3 | {% block js_extra %} 4 | var url = '{% url "wildewidgets_json" %}?wildewidgetclass={{wildewidgetclass}}{% if extra_data %}&extra_data={{extra_data}}{% endif %}&csrf_token={{csrf_token}}'; 5 | $.getJSON(url, function(data) { 6 | chart_{{css_id}}.updateSeries(data['series']); 7 | }); 8 | {% endblock %} -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/initials_avatar.html: -------------------------------------------------------------------------------- 1 | 2 | {{fullname}} 3 | {{fullname}}{{initials}} 4 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/apex_chart.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs==1.2.3 2 | jinja2==3.0.0 3 | pydata-sphinx-theme==0.9.0 # https://github.com/pydata/pydata-sphinx-theme 4 | Sphinx==5.2.3 # https://github.com/sphinx-doc/sphinx 5 | sphinxcontrib-images==0.9.4 # https://github.com/sphinx-contrib/images 6 | sphinxcontrib-django2 # https://github.com/timoludwig/sphinxcontrib-django2 7 | -------------------------------------------------------------------------------- /demo/demo/core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | # from .models import SomeModel 3 | 4 | # Register your models here to have them show up in the django admin. 5 | # NOTE: This is not useful for access.caltech apps outside of dev, though, as django admin uses URLs that depend on 6 | # the models' IDs, which doesn't fly with CIT_AUTH. So the django admin feature may just not be useful for an app. 7 | # admin.site.register(SomeModel) 8 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/header_with_controls.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{header_text}} 4 | {% if not badge_text == None %}{{badge_text}}{% endif %} 5 |
6 | {% block controls %} 7 | {% endblock %} 8 |
-------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | version: 2 4 | 5 | build: 6 | os: ubuntu-22.04 7 | apt_packages: 8 | - libssl-dev 9 | - libmysqlclient-dev 10 | - python3-dev 11 | tools: 12 | python: "3.11" 13 | 14 | sphinx: 15 | configuration: docs/conf.py 16 | 17 | python: 18 | install: 19 | - requirements: requirements.txt 20 | - method: pip 21 | path: . 22 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/block--simple.html: -------------------------------------------------------------------------------- 1 | <{{tag}}{% for key, value in attributes.items %} {{key}}="{{value}}"{% endfor %}{% if css_id is not None %} id="{{css_id}}"{% endif %}{% if css_classes is not None %} class="{{css_classes}}"{% endif %}{% for key, value in data_attributes.items %} data-bs-{{key}}="{{value}}"{% endfor %}{% for key, value in aria_attributes.items %} aria-{{key}}="{{value}}"{% endfor %}> 2 | 3 | {% if script %} 4 | 7 | {% endif %} 8 | -------------------------------------------------------------------------------- /docs/api_charts.rst: -------------------------------------------------------------------------------- 1 | Chart Widgets 2 | ============= 3 | 4 | 5 | Altair Charts 6 | ------------- 7 | 8 | .. automodule:: wildewidgets.widgets.charts.altair 9 | :members: 10 | :show-inheritance: 11 | 12 | 13 | 14 | Apex Charts 15 | ----------- 16 | 17 | .. automodule:: wildewidgets.widgets.charts.apex 18 | :members: 19 | :show-inheritance: 20 | 21 | 22 | 23 | ChartJS Charts 24 | -------------- 25 | 26 | .. automodule:: wildewidgets.widgets.charts.chartjs 27 | :members: 28 | :show-inheritance: -------------------------------------------------------------------------------- /demo/setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length: 120 3 | filename: *.py 4 | exclude: *.cfg, *.js, *.json, *.bak, *.md, *.sql, *.sh, *.txt, *.yml, simple_test_db, Makefile, Dockerfile, MANIFEST.in 5 | # E221: multiple spaces before operator 6 | # E241: multiple spaces after : 7 | # E265: block comment should start with '# ' 8 | # E266: too many leading '#' for block comment 9 | # E401: multiple imports on one line 10 | ignore = E221,E241,E265,E266,E401,W503,W504 11 | 12 | [mypy] 13 | python_executable: ~/.pyenv/shims/python 14 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/widget-list--main.html: -------------------------------------------------------------------------------- 1 | {% extends 'wildewidgets/block.html' %} 2 | {% load wildewidgets %} 3 | 4 | 5 | {% block block_content %} 6 | {% for widget in entries %} 7 | 8 |
9 | 10 | {% wildewidgets widget.get_title %} 11 |
12 | 13 |
14 | {% wildewidgets widget %} 15 |
16 | {% endfor %} 17 | {% endblock %} 18 | 19 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.2.4 3 | commit = True 4 | tag = True 5 | tag_name = {new_version} 6 | 7 | [bumpversion:file:pyproject.toml] 8 | 9 | [bumpversion:file:wildewidgets/__init__.py] 10 | 11 | [bumpversion:file:docs/conf.py] 12 | 13 | [bumpversion:file:demo/setup.py] 14 | 15 | [bumpversion:file:demo/Makefile] 16 | 17 | [bumpversion:file:demo/requirements.txt] 18 | search = django-wildewidgets=={current_version} 19 | replace = django-wildewidgets=={new_version} 20 | 21 | [bumpversion:file:demo/demo/__init__.py] 22 | 23 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/widget-list--sidebar.html: -------------------------------------------------------------------------------- 1 | {% extends 'wildewidgets/block.html' %} 2 | {% load wildewidgets %} 3 | 4 | {% block block_content %} 5 |
{{ title }}
6 |
7 | {% if widgets %} 8 | {% wildewidgets widgets %} 9 | {% endif %} 10 | {% if actions %} 11 | {% wildewidgets actions %} 12 | {% endif %} 13 | {% if widget_index %} 14 | {% wildewidgets widget_index %} 15 | {% endif %} 16 |
17 | {% endblock %} 18 | 19 | -------------------------------------------------------------------------------- /demo/etc/gunicorn_logging.conf: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root,gunicorn.error,gunicorn.access 3 | 4 | [handlers] 5 | keys=console,access_console 6 | 7 | [formatters] 8 | 9 | [logger_root] 10 | level=INFO 11 | handlers=console 12 | 13 | [logger_gunicorn.error] 14 | level=INFO 15 | handlers=console 16 | propagate=0 17 | qualname=gunicorn.error 18 | 19 | [logger_gunicorn.access] 20 | level=INFO 21 | handlers=access_console 22 | propagate=0 23 | qualname=gunicorn.access 24 | 25 | [handler_access_console] 26 | class=StreamHandler 27 | formatter=docker_access 28 | args=(sys.stdout, ) 29 | 30 | -------------------------------------------------------------------------------- /wildewidgets/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa: F403 2 | from .buttons import * # noqa: F403 3 | from .charts import * # noqa: F403 4 | from .datagrid import * # noqa: F403 5 | from .forms import * # noqa: F403 6 | from .grid import * # noqa: F403 7 | from .headers import * # noqa: F403 8 | from .icons import * # noqa: F403 9 | from .layout import * # noqa: F403 10 | from .misc import * # noqa: F403 11 | from .modals import * # noqa: F403 12 | from .navigation import * # noqa: F403 13 | from .structure import * # noqa: F403 14 | from .tables import * # noqa: F403 15 | from .text import * # noqa: F403 16 | -------------------------------------------------------------------------------- /demo/demo/core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | F = models.Field 4 | 5 | 6 | class Measurement(models.Model): 7 | name: F = models.CharField(max_length=128) 8 | time: F = models.DateTimeField( 9 | "Time", help_text="The date and time the measurement was made." 10 | ) 11 | pressure: F = models.DecimalField(max_digits=8, decimal_places=2) 12 | temperature: F = models.DecimalField(max_digits=8, decimal_places=2) 13 | restricted: F = models.BooleanField(default=False) 14 | open: F = models.BooleanField(default=True) 15 | 16 | def __str__(self) -> str: 17 | return f"Measurement({self.name}, {self.time})" 18 | -------------------------------------------------------------------------------- /demo/demo/settings_docker.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # These are the env vars that get retrieved with getenv(). They must be set to 4 | # avoid raising UnsetEnvironmentVariable. 5 | os.environ["DB_NAME"] = "fake" 6 | os.environ["DB_USER"] = "fake" 7 | os.environ["DB_PASSWORD"] = "fake" # noqa: S105 8 | os.environ["DB_HOST"] = "fake" 9 | os.environ["DB_PORT"] = "fake" 10 | os.environ["CACHE"] = "fake" 11 | # These are set to True here ONLY so the static resouces for 12 | # django-debug-toolbar will be collected during image build. 13 | os.environ["DEVELOPMENT"] = "True" 14 | os.environ["ENABLE_DEBUG_TOOLBAR"] = "True" 15 | 16 | # noinspection PyUnresolvedReferences 17 | from .settings import * # noqa: F403 18 | -------------------------------------------------------------------------------- /docs/api_tables.rst: -------------------------------------------------------------------------------- 1 | Table Widgets 2 | ============= 3 | 4 | Tables 5 | ------ 6 | 7 | .. automodule:: wildewidgets.widgets.tables.tables 8 | :members: 9 | :show-inheritance: 10 | 11 | Components 12 | ---------- 13 | 14 | .. automodule:: wildewidgets.widgets.tables.components 15 | :members: 16 | :show-inheritance: 17 | 18 | Base 19 | ---- 20 | 21 | .. automodule:: wildewidgets.widgets.tables.base 22 | :members: 23 | :show-inheritance: 24 | 25 | Views 26 | ----- 27 | 28 | .. automodule:: wildewidgets.widgets.tables.views 29 | :members: 30 | :show-inheritance: 31 | 32 | Actions 33 | ------- 34 | 35 | .. automodule:: wildewidgets.widgets.tables.actions 36 | :members: 37 | :show-inheritance: -------------------------------------------------------------------------------- /demo/demo/users/migrations/0002_load_fixture.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.2 on 2022-11-09 23:37 2 | 3 | from django.db import migrations 4 | from django.core.management import call_command 5 | 6 | fixture = 'users' 7 | 8 | 9 | def load_fixture(apps, schema_editor): 10 | call_command('loaddata', fixture, app_label='users') 11 | 12 | 13 | def unload_fixture(apps, schema_editor): 14 | User = apps.get_model("user", "User") 15 | User.objects.all().delete() 16 | 17 | 18 | class Migration(migrations.Migration): 19 | 20 | dependencies = [ 21 | ('users', '0001_initial'), 22 | ] 23 | 24 | operations = [ 25 | migrations.RunPython(load_fixture, reverse_code=unload_fixture), 26 | ] 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | RAWVERSION = $(filter-out __version__ = , $(shell grep __version__ wildewidgets/__init__.py)) 2 | VERSION = $(strip $(shell echo $(RAWVERSION))) 3 | 4 | PACKAGE = django-wildewidgets 5 | 6 | clean: 7 | rm -rf *.tar.gz dist build *.egg-info *.rpm 8 | find . -name "*.pyc" | xargs rm 9 | find . -name "__pycache__" | xargs rm -rf 10 | 11 | version: 12 | @echo $(VERSION) 13 | 14 | dist: clean 15 | @python -m build 16 | 17 | release: dist 18 | @bin/release.sh 19 | 20 | compile: uv.lock 21 | @uv pip compile --group demo --group docs --group test pyproject.toml -o requirements.txt 22 | 23 | tox: 24 | # create a tox pyenv virtualenv based on 2.7.x 25 | # install tox and tox-pyenv in that ve 26 | # actiave that ve before running this 27 | @tox 28 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | DJANGO_SETTINGS_MODULE = wildewidgets.settings 11 | 12 | # Put it first so that "make" without argument is like "make help". 13 | help: 14 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 15 | 16 | .PHONY: help Makefile 17 | 18 | # Catch-all target: route all unknown targets to Sphinx using the new 19 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 20 | %: Makefile 21 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 22 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/block.html: -------------------------------------------------------------------------------- 1 | {% load wildewidgets %} 2 | <{{tag}}{% for key, value in attributes.items %} {{key}}="{{value}}"{% endfor %}{% if css_id is not None %} id="{{css_id}}"{% endif %}{% if css_classes is not None %} class="{{css_classes}}"{% endif %}{% for key, value in data_attributes.items %} data-bs-{{key}}="{{value}}"{% endfor %}{% for key, value in aria_attributes.items %} aria-{{key}}="{{value}}"{% endfor %}> 3 | {% block block_content %} 4 | {% for content in blocks %} 5 | {% if content|is_wildewidget %} 6 | {% wildewidgets content %} 7 | {% else %} 8 | {{content|safe}} 9 | {% endif %} 10 | {% endfor %} 11 | {% endblock %} 12 | 13 | 14 | {% if script %} 15 | 18 | {% endif %} -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/card_block.html: -------------------------------------------------------------------------------- 1 | {% extends 'wildewidgets/block.html' %} 2 | {% load wildewidgets %} 3 | 4 | {% block block_content %} 5 | {% if header or header_text %} 6 |
7 | {% if header %} 8 | {% wildewidgets header %} 9 | {% elif header_text %} 10 | {{ header_text}} 11 | {% endif %} 12 |
13 | {% endif %} 14 |
15 | {% if title %} 16 |
{{ title }}
17 | {% if subtitle %} 18 |
{{ subtitle }}
19 | {% endif %} 20 | {% endif %} 21 |
22 | {% wildewidgets widget %} 23 |
24 |
25 | {% endblock %} -------------------------------------------------------------------------------- /wildewidgets/static/wildewidgets/css/table_extra.css: -------------------------------------------------------------------------------- 1 | table { 2 | color: #666 !important; 3 | } 4 | table.wildewidgets-table { 5 | margin-top: 5px; 6 | } 7 | td, th, tbody { 8 | border-bottom-color: #eee !important; 9 | } 10 | table.dataTable.no-footer { 11 | border-bottom: 1px solid #eee; 12 | } 13 | tbody { 14 | border-top: 1px solid #eee !important; 15 | } 16 | 17 | .dataTables_info { 18 | padding-left: 15px !important; 19 | } 20 | 21 | button.toggle-vis.btn-outline-secondary:not(.active) { 22 | color: #fff; 23 | background-color: #6c757d; 24 | border-color: #6c757d; 25 | } 26 | td { 27 | vertical-align: middle !important; 28 | } 29 | 30 | table.dataTable.table-sm tbody th, 31 | table.dataTable.table-sm tbody td { 32 | padding: .25rem 0.25rem; 33 | } 34 | -------------------------------------------------------------------------------- /demo/etc/supervisord.conf: -------------------------------------------------------------------------------- 1 | [unix_http_server] 2 | file=/tmp/supervisor.sock 3 | 4 | [supervisord] 5 | nodaemon=true 6 | logfile=/dev/fd/1 7 | logfile_maxbytes=0 8 | pidfile=/tmp/supervisord.pid 9 | user=app 10 | 11 | [program:gunicorn] 12 | command=gunicorn --ssl-version 2 --config /app/demo/gunicorn_config.py demo.wsgi 13 | user=app 14 | directory=/app 15 | stdout_logfile=/dev/stdout 16 | stdout_logfile_maxbytes=0 17 | redirect_stderr=true 18 | 19 | [program:nginx] 20 | user=app 21 | command=/usr/sbin/nginx -c /etc/nginx/nginx.conf 22 | stdout_logfile=/dev/stdout 23 | stdout_logfile_maxbytes=0 24 | redirect_stderr=true 25 | 26 | [rpcinterface:supervisor] 27 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 28 | 29 | [supervisorctl] 30 | serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket 31 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/altairchart.html: -------------------------------------------------------------------------------- 1 | {% if options.title %} 2 |
3 |

{{ options.title }}

4 |
5 | {% endif %} 6 |
7 |
8 |
9 | 24 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/modal.html: -------------------------------------------------------------------------------- 1 | {% load wildewidgets %} 2 | 19 | {% if script %} 20 | 21 | {% endif %} -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/page_tab_block.html: -------------------------------------------------------------------------------- 1 | {% extends 'wildewidgets/block.html' %} 2 | {% load wildewidgets %} 3 | 4 | {% block block_content %} 5 | 20 | 21 |
22 | 30 |
31 | {% endblock %} -------------------------------------------------------------------------------- /demo/demo/core/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import ( 4 | AltairView, 5 | ApexChartView, 6 | ChartView, 7 | HomeView, 8 | ListWidgetView, 9 | ModalView, 10 | StructureWidgetView, 11 | TableView, 12 | TextWidgetView, 13 | ) 14 | 15 | # These URLs are loaded by demo/urls.py. 16 | app_name = "core" 17 | urlpatterns = [ 18 | path("", HomeView.as_view(), name="home"), 19 | path("altair", AltairView.as_view(), name="altair"), 20 | path("tables", TableView.as_view(), name="tables"), 21 | path("apex", ApexChartView.as_view(), name="apex"), 22 | path("text", TextWidgetView.as_view(), name="text"), 23 | path("list", ListWidgetView.as_view(), name="list"), 24 | path("structure", StructureWidgetView.as_view(), name="structure"), 25 | path("modal", ModalView.as_view(), name="modal"), 26 | path("charts", ChartView.as_view(), name="charts"), 27 | ] 28 | -------------------------------------------------------------------------------- /demo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | def main(): 7 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo.settings') 8 | try: 9 | from django.core.management import execute_from_command_line 10 | except ImportError as exc: 11 | raise ImportError( 12 | "Couldn't import Django. Are you sure it's installed and " 13 | "available on your PYTHONPATH environment variable? Did you " 14 | "forget to activate a virtual environment?" 15 | ) from exc 16 | 17 | # This allows easy placement of apps within the interior seedling directory. 18 | current_path = os.path.dirname(os.path.abspath(__file__)) 19 | sys.path.append(os.path.join(current_path, 'demo')) 20 | 21 | execute_from_command_line(sys.argv) 22 | 23 | 24 | if __name__ == '__main__': 25 | main() 26 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /wildewidgets/static/wildewidgets/css/_toggleablemanytomanyfieldblock.scss: -------------------------------------------------------------------------------- 1 | .toggle-form-block { 2 | .card-body { 3 | padding-left: 0; 4 | padding-right: 0; 5 | } 6 | .button-holder { 7 | padding-left: var(--tblr-card-spacer-x); 8 | padding-right: var(--tblr-card-spacer-x); 9 | 10 | } 11 | .form-switch.form-check-reverse { 12 | padding-right: 0; 13 | } 14 | .form-check .form-check{ 15 | padding-left: 2.5rem; 16 | padding-right: var(--tblr-card-spacer-x); 17 | padding-top: 0.75rem; 18 | padding-bottom: 0.75rem; 19 | border-bottom: 1px solid #f0f0f0; 20 | margin-bottom: 0; 21 | .form-check-input { 22 | margin-right: 0.5rem; 23 | } 24 | label { 25 | text-align: left; 26 | width: 100%; 27 | } 28 | &:hover { 29 | background-color: #f6f6f6; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /demo/demo/gunicorn_config.py: -------------------------------------------------------------------------------- 1 | import environ 2 | 3 | env = environ.Env() 4 | 5 | # general 6 | bind = "unix:/tmp/demo.sock" 7 | workers = 8 8 | worker_class = "sync" 9 | daemon = False 10 | timeout = 300 11 | worker_tmp_dir = "/tmp" # noqa: S108 12 | 13 | # requires futures module for threads > 1 14 | threads = 1 15 | 16 | # During development, this will cause the server to reload when the code changes. 17 | # noinspection PyShadowingBuiltins 18 | reload = env.bool("GUNICORN_RELOAD", default=False) 19 | 20 | # Logging. 21 | accesslog = "-" 22 | access_log_format = ( 23 | '%({X-Forwarded-For}i)s %(l)s %(u)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s' 24 | ) 25 | errorlog = "-" 26 | syslog = False 27 | 28 | # statsd settings need to have defaults provided, because dev sites don't use statsd. 29 | host = env("STATSD_HOST", default=None) 30 | port = env.int("STATSD_PORT", default=8125) 31 | statsd_host = f"{host}:{port}" if host and port else None 32 | statsd_prefix = env("STATSD_PREFIX", default=None) 33 | -------------------------------------------------------------------------------- /wildewidgets/static/wildewidgets/js/wildewidgets.js: -------------------------------------------------------------------------------- 1 | 2 | function register_ww_dropdown_toggle() { 3 | /* 4 | Register our onClick handler for .dropdown-toggle with a data attribute 5 | of "data-bs-toggle='dropdown-ww'". 6 | 7 | This is what makes :py:class:`ClickableNavDropdownControl` work. 8 | */ 9 | $(".dropdown-toggle[data-bs-toggle='dropdown-ww']").click(function () { 10 | var target_id = $(this).attr('data-bs-target'); 11 | var target = $(target_id); 12 | if ($(this).attr('aria-expanded') === 'false') { 13 | target.addClass('show'); 14 | $(this).attr('aria-expanded', 'true'); 15 | } else { 16 | $(this).attr('aria-expanded', 'false'); 17 | target.removeClass('show'); 18 | } 19 | }) 20 | 21 | } 22 | 23 | // ---------------------------------- 24 | // Document ready 25 | // ---------------------------------- 26 | 27 | $(document).ready(function() { 28 | register_ww_dropdown_toggle(); 29 | }); 30 | -------------------------------------------------------------------------------- /demo/demo/core/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.8 on 2020-12-17 21: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='Measurement', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=128)), 19 | ('time', models.DateTimeField(help_text='The date and time the measurement was made.', verbose_name='Time')), 20 | ('pressure', models.DecimalField(decimal_places=2, max_digits=8)), 21 | ('temperature', models.DecimalField(decimal_places=2, max_digits=8)), 22 | ('restricted', models.BooleanField(default=False)), 23 | ('open', models.BooleanField(default=True)), 24 | ], 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /demo/sql/docker/my.cnf: -------------------------------------------------------------------------------- 1 | # The server collation here does not match our AWS RDS exactly. In AWS one must use the 2 | # utf8mb4_unicode_ci collation for the server even though you can set the default database 3 | # collation to be utf8mb4_0900_ai_ci. In the container, you can set the server collation 4 | # here but not the database collation. So I decided to set the server collation for our 5 | # docker dbs in case someone forgets to set the character set and collation when creating a db. 6 | [mysqld] 7 | character_set_server=utf8mb4 8 | collation_server=utf8mb4_0900_ai_ci 9 | # AWS uses the non-default mysql_native_password plugin; make our dev db match 10 | default-authentication-plugin=mysql_native_password 11 | 12 | 13 | # This tells mysql that our underlying volume is case-insensitive 14 | lower_case_table_names=2 15 | 16 | 17 | # If you connect to the database from our application, we get utf8mb4 connections. Set this 18 | # so the same thing happens when using the mysql client from within the database container. 19 | [client] 20 | default-character-set=utf8mb4 21 | -------------------------------------------------------------------------------- /demo/demo/core/templates/core/intermediate.html: -------------------------------------------------------------------------------- 1 | {% extends 'academy_theme/base--wildewidgets.html' %} 2 | {% load i18n static %} 3 | 4 | {% block extra_css %} 5 | {{block.super}} 6 | 7 | {% endblock %} 8 | 9 | {% block extra_footer_js %} 10 | 11 | 12 | 13 | 14 | 15 | {% endblock %} 16 | 17 | {% block ribbon_bar_links %} 18 |
19 |  Source Code 20 | Documentation 21 |
22 | {% endblock %} -------------------------------------------------------------------------------- /wildewidgets/templatetags/wildewidgets.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | from ..widgets import Widget 4 | 5 | 6 | register = template.Library() 7 | 8 | 9 | class WildewidgetsNode(template.Node): 10 | 11 | def __init__(self, widget): 12 | self.widget = widget 13 | 14 | def render(self, context): 15 | if self not in context.render_context: 16 | context.render_context[self] = ( 17 | template.Variable(self.widget) 18 | ) 19 | widget = context.render_context[self] 20 | actual_widget = widget.resolve(context) 21 | node_context = context.__copy__() 22 | flattened = node_context.flatten() 23 | content = actual_widget.get_content(**flattened) 24 | return content 25 | 26 | 27 | @register.tag(name="wildewidgets") 28 | def do_wildewidget_render(parser, token): 29 | token = token.split_contents() 30 | widget = token.pop(1) 31 | return WildewidgetsNode(widget) 32 | 33 | 34 | @register.filter 35 | def is_wildewidget(obj): 36 | return isinstance(obj, Widget) 37 | -------------------------------------------------------------------------------- /demo/demo/users/templates/users/intermediate.html: -------------------------------------------------------------------------------- 1 | {% extends 'academy_theme/base.html' %} 2 | {% load academy_theme i18n static sass_tags %} 3 | 4 | {% block title %}{% trans 'Book Manager Demo' %}{% endblock %} 5 | 6 | {% block extra_css %} 7 | 8 | 9 | 10 | {% endblock %} 11 | 12 | {% block extra_header_js %} 13 | 14 | 15 | 16 | {% endblock %} 17 | 18 | {% block breadcrumbs %} 19 | 24 | {% endblock %} -------------------------------------------------------------------------------- /demo/demo/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for demo 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/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | import sys 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | 15 | # This allows easy placement of apps within the interior demo directory. 16 | app_path = os.path.abspath( # noqa: PTH100 17 | os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir) # noqa: PTH100, PTH118, PTH120 18 | ) 19 | sys.path.append(os.path.join(app_path, "demo")) # noqa: PTH118 20 | 21 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 22 | # if running multiple sites in the same mod_wsgi process. To fix this, use 23 | # mod_wsgi daemon mode with each site in its own daemon process, or use 24 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") 25 | 26 | # This application object is used by any WSGI server configured to use this file. 27 | application = get_wsgi_application() 28 | 29 | # Apply WSGI middleware here. 30 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/tab_block.html: -------------------------------------------------------------------------------- 1 | {% extends 'wildewidgets/block.html' %} 2 | {% load wildewidgets %} 3 | 4 | {% block block_content %} 5 | 22 | 23 |
24 | 36 |
37 | {% endblock %} -------------------------------------------------------------------------------- /demo/etc/environment.txt: -------------------------------------------------------------------------------- 1 | DB_NAME=demo 2 | DB_HOST=db 3 | DB_USER=demo_u 4 | DB_PASSWORD=password 5 | 6 | DJANGO_SECRET_KEY=__SECRET_KEY__ 7 | 8 | ############################################################################ 9 | # DEV SETTINGS - Don't put anything below this line in your prod environment 10 | ############################################################################ 11 | # DEBUG is the django setting. DEVELOPMENT exists to let us specify "dev mode" even when DEBUG is False. 12 | # This let you do "Developmenty" things, like disable emails, even while DEBUG needs to be False for testing stuff. 13 | DEBUG=True 14 | DEVELOPMENT=True 15 | # You should set CACHE_TEMPLATES to False in dev, but leave it undefined in prod. Set it to True in dev for 16 | # those unusual times when you want to cache templates, like when using StreamField-heavy editors a lot. 17 | CACHE_TEMPLATES=False 18 | GUNICORN_RELOAD=True 19 | # Set this to True to disable all caching. 20 | DISABLE_CACHE=False 21 | # Disable the creation of .pyc files. 22 | PYTHONDONTWRITEBYTECODE=1 23 | # Set either of these to True to enable django-queryinspect and/or django-debug-toolbar. 24 | # Note that these two are forced to False unless DEVELOPMENT is True. 25 | ENABLE_QUERYINSPECT=False 26 | ENABLE_DEBUG_TOOLBAR=False 27 | -------------------------------------------------------------------------------- /demo/demo/users/fixtures/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "users.user", 4 | "pk": 1, 5 | "fields": { 6 | "password": "pbkdf2_sha256$390000$4DUh93PxINVgpp5wVECyQf$08LpgusQxOQMa/p3nJmputEDfApyCHDIxZ60a40c6tM=", 7 | "last_login": null, 8 | "is_superuser": true, 9 | "username": "root", 10 | "first_name": "", 11 | "last_name": "", 12 | "email": "demo-admin@example.com", 13 | "is_staff": true, 14 | "is_active": true, 15 | "date_joined": "2022-08-28T23:58:36.244Z", 16 | "groups": [], 17 | "user_permissions": [] 18 | } 19 | }, 20 | { 21 | "model": "users.user", 22 | "pk": 3, 23 | "fields": { 24 | "password": "pbkdf2_sha256$390000$jvImzEpYULVHsyuLIo8CQs$c6RTpkEiSzHMwhAfI9hPKxRP8zu5bR1zI3HeXmO8hN8=", 25 | "last_login": null, 26 | "is_superuser": false, 27 | "username": "testy", 28 | "first_name": "Testy", 29 | "last_name": "McTest", 30 | "email": "testy.mctest@example.com", 31 | "is_staff": true, 32 | "is_active": true, 33 | "date_joined": "2022-09-28T23:59:18.190Z", 34 | "groups": [], 35 | "user_permissions": [] 36 | } 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /demo/demo/core/fixtures/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "users.user", 4 | "pk": 1, 5 | "fields": { 6 | "password": "pbkdf2_sha256$390000$4DUh93PxINVgpp5wVECyQf$08LpgusQxOQMa/p3nJmputEDfApyCHDIxZ60a40c6tM=", 7 | "last_login": null, 8 | "is_superuser": true, 9 | "username": "root", 10 | "first_name": "", 11 | "last_name": "", 12 | "email": "book-manager-admin@example.com", 13 | "is_staff": true, 14 | "is_active": true, 15 | "date_joined": "2022-08-28T23:58:36.244Z", 16 | "groups": [], 17 | "user_permissions": [] 18 | } 19 | }, 20 | { 21 | "model": "users.user", 22 | "pk": 3, 23 | "fields": { 24 | "password": "pbkdf2_sha256$390000$jvImzEpYULVHsyuLIo8CQs$c6RTpkEiSzHMwhAfI9hPKxRP8zu5bR1zI3HeXmO8hN8=", 25 | "last_login": null, 26 | "is_superuser": false, 27 | "username": "testy", 28 | "first_name": "Testy", 29 | "last_name": "McTest", 30 | "email": "testy.mctest@example.com", 31 | "is_staff": true, 32 | "is_active": true, 33 | "date_joined": "2022-09-28T23:59:18.190Z", 34 | "groups": [], 35 | "user_permissions": [] 36 | } 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /bin/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if test $(git rev-parse --abbrev-ref HEAD) = "main"; then 4 | if test -z "$(git status --untracked-files=no --porcelain)"; then 5 | MSG="$(git log -1 --pretty=%B)" 6 | echo "$MSG" | grep "Bump version" 7 | if test $? -eq 0; then 8 | VERSION=$(echo "$MSG" | awk -F→ '{print $2}') 9 | echo "---------------------------------------------------" 10 | echo "Releasing version ${VERSION} ..." 11 | echo "---------------------------------------------------" 12 | echo 13 | echo 14 | git checkout build 15 | git merge main 16 | echo "Pushing build to origin ..." 17 | git push --tags deploy build 18 | git checkout main 19 | git push --tags deploy main 20 | echo "Pushing main to origin ..." 21 | git push --tags origin main 22 | echo "Uploading to PyPI ..." 23 | twine upload dist/* 24 | else 25 | echo "Last commit was not a bumpversion; aborting." 26 | echo "Last commit message: ${MSG}" 27 | fi 28 | else 29 | git status 30 | echo 31 | echo 32 | echo "------------------------------------------------------" 33 | echo "You have uncommitted changes; aborting." 34 | echo "------------------------------------------------------" 35 | fi 36 | else 37 | echo "You're not on main; aborting." 38 | fi 39 | -------------------------------------------------------------------------------- /demo/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | demo: 5 | image: "wildewidgets_demo:latest" 6 | container_name: "wildewidgets_demo" 7 | platform: linux/amd64 8 | restart: always 9 | hostname: "wildewidgets_demo" 10 | ports: 11 | - 443:443 12 | environment: 13 | - DEBUG=True 14 | - DEVELOPMENT=True 15 | - GUNICORN_RELOAD=True 16 | depends_on: 17 | - mysql 18 | command: bin/wait-for-it.sh mysql:3306 --and /opt/python/bin/supervisord 19 | volumes: 20 | - .:/app 21 | - ../wildewidgets:/ve/lib/python3.10/site-packages/wildewidgets 22 | 23 | mysql: 24 | image: mysql:8.0.23 25 | container_name: "db" 26 | platform: linux/amd64 27 | environment: 28 | MYSQL_ROOT_PASSWORD: root_password 29 | # Apply the MySQL 5.6.40+ default sql_modes, which are not enabled in Docker's MySQL containers, even in 5.6.49. 30 | command: mysqld --sql_mode="REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,ANSI,STRICT_TRANS_TABLES" 31 | ports: 32 | # Expose port 3306 on the container as port 3307 on the host, so that 33 | # sql clients can connect to it. 34 | - 3307:3306 35 | volumes: 36 | - ./sql/docker/my.cnf:/etc/mysql/conf.d/dev.cnf 37 | - ./sql/docker:/docker-entrypoint-initdb.d 38 | - wildewidgets_demo_data:/var/lib/mysql 39 | 40 | volumes: 41 | # The Docker volume in which the database's files are stored. Works in tandem 42 | # with the "wildewidgets_demo_data:/var/lib/mysql" volume mount defined above. 43 | wildewidgets_demo_data: 44 | -------------------------------------------------------------------------------- /demo/demo/core/migrations/0002_initial_data.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.core.management import call_command 3 | from django.db import migrations 4 | 5 | measurement_fixture = 'measurements' 6 | book_manager_fixture = 'book_manager' 7 | user_fixture = 'users' 8 | 9 | 10 | def load_fixture(apps, schema_editor): 11 | call_command('loaddata', measurement_fixture, app_label='core') 12 | call_command('loaddata', user_fixture, app_label='core') 13 | call_command('loaddata', book_manager_fixture, app_label='core') 14 | 15 | 16 | def unload_fixture(apps, schema_editor): 17 | Measurement = apps.get_model("core", "Measurement") 18 | Measurement.objects.all().delete() 19 | Publisher = apps.get_model("book_manager", "Publisher") 20 | Publisher.objects.all().delete() 21 | Binding = apps.get_model("book_manager", "Binding") 22 | Binding.objects.all().delete() 23 | Book = apps.get_model("book_manager", "Book") 24 | Book.objects.all().delete() 25 | Author = apps.get_model("book_manager", "Author") 26 | Author.objects.all().delete() 27 | Shelf = apps.get_model("book_manager", "Shelf") 28 | Shelf.objects.all().delete() 29 | Reading = apps.get_model("book_manager", "Reading") 30 | Reading.objects.all().delete() 31 | User = get_user_model() 32 | User.objects.all().delete() 33 | 34 | 35 | class Migration(migrations.Migration): 36 | 37 | dependencies = [ 38 | ('core', '0001_initial'), 39 | ] 40 | 41 | operations = [ 42 | migrations.RunPython(load_fixture, reverse_code=unload_fixture), 43 | ] -------------------------------------------------------------------------------- /demo/Makefile: -------------------------------------------------------------------------------- 1 | VERSION = 1.2.4 2 | 3 | PACKAGE = wildewidgets_demo 4 | 5 | .PHONY: clean dist build force-build tag dev dev-detached devup devdown logall log exec restart docker-clean docker-destroy-db docker-destroy list package 6 | #====================================================================== 7 | 8 | clean: 9 | rm -rf *.tar.gz dist *.egg-info *.rpm 10 | find . -name "*.pyc" -exec rm '{}' ';' 11 | 12 | dist: clean 13 | @python setup.py sdist 14 | 15 | package: 16 | (cd ..; python setup.py sdist) 17 | cp ../dist/django-wildewidgets-${VERSION}.tar.gz django-wildewidgets.tar.gz 18 | 19 | build: 20 | docker build -t ${PACKAGE}:${VERSION} . 21 | docker tag ${PACKAGE}:${VERSION} ${PACKAGE}:latest 22 | docker image prune -f 23 | 24 | force-build: package 25 | docker build --no-cache -t ${PACKAGE}:${VERSION} . 26 | docker tag ${PACKAGE}:${VERSION} ${PACKAGE}:latest 27 | docker image prune -f 28 | 29 | dev: 30 | docker-compose -f docker-compose.yml up 31 | 32 | dev-detached: 33 | docker-compose -f docker-compose.yml up -d 34 | 35 | down: devdown 36 | 37 | devup: 38 | docker-compose -f docker-compose.yml up -d 39 | 40 | devdown: 41 | docker-compose down 42 | 43 | logall: 44 | docker-compose logs -f 45 | 46 | log: 47 | docker logs -f wildewidgets_demo 48 | 49 | exec: 50 | docker exec -it wildewidgets_demo /bin/bash 51 | 52 | docker-clean: 53 | docker stop $(shell docker ps -a -q) 54 | docker rm $(shell docker ps -a -q) 55 | 56 | docker-destroy-db: docker-clean 57 | docker volume rm wildewidgets_demo_data 58 | 59 | 60 | .PHONY: list 61 | list: 62 | @$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | xargs 63 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/doughnutchart_json.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 49 | 50 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. django-wildewidgets documentation master file, created by 2 | sphinx-quickstart on Mon May 31 15:07:41 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | django-wildewidgets documentation 7 | ================================= 8 | 9 | django-wildewidgets is a Django design library providing several tools for building 10 | full-featured, widget-based web applications with a standard, consistent design, based 11 | on Bootstrap. 12 | 13 | Features include: 14 | 15 | * Large library of standard widgets 16 | * Custom widgets 17 | * Widgets can be composable 18 | * Teplateless design 19 | * AJAX data for tables, charts, and other data based widgets 20 | * Several supporting views 21 | 22 | The standard library widgets include: 23 | 24 | * Basic blocks 25 | * Template based widgets 26 | * Basic Buttons 27 | * Form, Modal, and Collapse Buttons 28 | * Header widgets 29 | * Chart widgets, including Altair, Apex, and ChartJS 30 | * Layout and structural widgets, like Card and Tab widgets 31 | * Modal widgets 32 | * Form widgets 33 | * Table widgets 34 | * Text widgets, like HTML, Code, Markdown, and Label widgets 35 | * Other miscillaneous widgets, like Breadcrumb, Gravatar, and KeyValueList widgets. 36 | 37 | | 38 | 39 | `DEMO `_ 40 | 41 | | 42 | 43 | .. thumbnail:: demo_ss_home.png 44 | :width: 200px 45 | :height: 173px 46 | 47 | .. thumbnail:: demo_ss_simple_table.png 48 | :width: 232px 49 | :height: 173px 50 | 51 | | 52 | 53 | .. toctree:: 54 | :maxdepth: 2 55 | :caption: Contents: 56 | 57 | install 58 | guide 59 | api 60 | 61 | Indices and tables 62 | ================== 63 | 64 | * :ref:`genindex` 65 | * :ref:`modindex` 66 | * :ref:`search` 67 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/paged_model_widget.html: -------------------------------------------------------------------------------- 1 | {% extends 'wildewidgets/block.html' %} 2 | {% load wildewidgets %} 3 | 4 | {% block block_content %} 5 | 6 | {% for widget in widget_list %} 7 | {% wildewidgets widget %} 8 | {% empty %} 9 | 10 | {% endfor %} 11 | 12 | {% if is_paginated %} 13 | 47 | {% endif %} 48 | 49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/doughnutchart.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 57 | 58 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | volumes 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | 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 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | *.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | # Development artifacts 109 | .python-version 110 | .DS_Store 111 | /*.sql 112 | config.codekit3 113 | sql/docker/mysql-data 114 | .vscode 115 | demo.code-workspace 116 | 117 | # Vim 118 | *.sw* 119 | *.bak 120 | 121 | # Terraform 122 | .terraform 123 | tags 124 | supervisord.pid 125 | requirements.txt.new 126 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2011-22 California Institute of Technology. Questions or comments 2 | may be directed to the author, the Academic Development Services group of 3 | Caltech's Information Management Systems and Services department, at 4 | imss-ads-staff@caltech.edu. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 7 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 8 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 9 | persons to whom the Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 12 | Software. 13 | 14 | Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | django-datatables-view license notice: 18 | 19 | MIT License 20 | 21 | Copyright (c) 2022 Maciej Wiśniowski 22 | 23 | Permission is hereby granted, free of charge, to any person obtaining a copy 24 | of this software and associated documentation files (the "Software"), to deal 25 | in the Software without restriction, including without limitation the rights 26 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 27 | copies of the Software, and to permit persons to whom the Software is 28 | furnished to do so, subject to the following conditions: 29 | 30 | The above copyright notice and this permission notice shall be included in all 31 | copies or substantial portions of the Software. 32 | 33 | 34 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 35 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 36 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 37 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 38 | 39 | 40 | -------------------------------------------------------------------------------- /.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 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | reports 50 | results.xml 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | # Development artifacts 109 | .python-version 110 | .DS_Store 111 | /*.sql 112 | config.codekit3 113 | sql/docker/mysql-data 114 | sandbox/data/private/ 115 | 116 | # Vim 117 | *.sw* 118 | *.bak 119 | tags 120 | 121 | # Terraform 122 | .terraform 123 | tags 124 | supervisord.pid 125 | requirements.txt.new 126 | 127 | # Ignore the config.codekit3 file -- it changes constantly 128 | config.codekit3 129 | 130 | # Ignore Visual Studio Code and PyCharm workspace and config 131 | *.code-workspace 132 | .idea 133 | .vscode 134 | 135 | # Local temp files 136 | local_storage 137 | 138 | django-wildewidgets.tar.gz 139 | -------------------------------------------------------------------------------- /docs/scientific_charts.rst: -------------------------------------------------------------------------------- 1 | ***************** 2 | Scientific Charts 3 | ***************** 4 | 5 | Usage 6 | ===== 7 | 8 | Without AJAX 9 | ------------ 10 | 11 | In your view code, import the ``AltairChart`` class, and the ``pandas`` and ``altair`` libraries (the pandas library and other requirements will be automatically installed when installing the altair library):: 12 | 13 | import pandas as pd 14 | import altair as alt 15 | from wildewidgets import AltairChart 16 | 17 | and define the chart in your view:: 18 | 19 | class AltairView(TemplateView): 20 | template_name = "core/altair.html" 21 | 22 | def get_context_data(self, **kwargs): 23 | data = pd.DataFrame({ 24 | 'a': list('CCCDDDEEE'), 25 | 'b': [2, 7, 4, 1, 2, 6, 8, 4, 7] 26 | } 27 | ) 28 | spec = alt.Chart(data).mark_point().encode( 29 | x='a', 30 | y='b' 31 | ) 32 | chart = AltairChart(title='Scientific Proof') 33 | chart.set_data(spec) 34 | kwargs['chart'] = chart 35 | return super().get_context_data(**kwargs) 36 | 37 | In your template, display the chart:: 38 | 39 | {{chart}} 40 | 41 | 42 | With AJAX 43 | --------- 44 | 45 | Create a file called ``wildewidgets.py`` in your app directory if it doesn't exist already and create a new class derived from the `AltairChart` class. You'll need to either override the ``load`` method, where you'll define your altair chart:: 46 | 47 | import pandas as pd 48 | import altair as alt 49 | from wildewidgets import AltairChart 50 | 51 | class SciChart(AltairChart): 52 | 53 | def load(self): 54 | data = pd.DataFrame({ 55 | 'a': list('CCCDDDEEE'), 56 | 'b': [2, 7, 4, 1, 2, 6, 8, 4, 10] 57 | } 58 | ) 59 | spec = alt.Chart(data).mark_point().encode( 60 | x='a', 61 | y='b' 62 | ) 63 | self.set_data(spec) 64 | 65 | Then in your view code, use this class instead:: 66 | 67 | from .wildewidgets import SciChart 68 | 69 | class HomeView(TemplateView): 70 | template_name = "core/altair.html" 71 | 72 | def get_context_data(self, **kwargs): 73 | kwargs['scichart'] = SciChart() 74 | return super().get_context_data(**kwargs) 75 | 76 | In your template, display the chart:: 77 | 78 | {{scichart}} 79 | 80 | Options 81 | ======= 82 | 83 | Most of the options of a scientific chart or graph are set in the Altair code, but there are a few that can be set here:: 84 | 85 | width: chart width (default: 400px) 86 | height: chart height (default: 300px) 87 | title: title text (default: None) 88 | -------------------------------------------------------------------------------- /docs/api_views.rst: -------------------------------------------------------------------------------- 1 | View Classes 2 | ============ 3 | 4 | ViewSets 5 | -------- 6 | 7 | .. automodule:: wildewidgets.viewsets 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | 13 | View Mixins 14 | ----------- 15 | 16 | .. autoclass:: wildewidgets.views.generic.GenericViewMixin 17 | :members: 18 | :undoc-members: 19 | :show-inheritance: 20 | 21 | .. autoclass:: wildewidgets.views.generic.GenericDatatableMixin 22 | :members: 23 | :undoc-members: 24 | :show-inheritance: 25 | 26 | .. autoclass:: wildewidgets.views.mixins.WidgetInitKwargsMixin 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | .. autoclass:: wildewidgets.views.mixins.JSONResponseMixin 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | .. autoclass:: wildewidgets.views.mixins.StandardWidgetMixin 37 | :members: 38 | :undoc-members: 39 | :show-inheritance: 40 | 41 | .. autoclass:: wildewidgets.views.permission.PermissionRequiredMixin 42 | :members: 43 | :undoc-members: 44 | :show-inheritance: 45 | 46 | AJAX Views 47 | ---------- 48 | 49 | .. autoclass:: wildewidgets.views.json.JSONResponseView 50 | :members: 51 | :undoc-members: 52 | :show-inheritance: 53 | 54 | .. autoclass:: wildewidgets.views.json.JSONDataView 55 | :members: 56 | :undoc-members: 57 | :show-inheritance: 58 | 59 | .. autoclass:: wildewidgets.views.json.WildewidgetDispatch 60 | :members: 61 | :undoc-members: 62 | :show-inheritance: 63 | 64 | 65 | Table Related Views 66 | ------------------- 67 | 68 | .. autoclass:: wildewidgets.views.tables.TableActionFormView 69 | :members: 70 | :undoc-members: 71 | :show-inheritance: 72 | 73 | .. autoclass:: wildewidgets.views.tables.TableView 74 | :members: 75 | :undoc-members: 76 | :show-inheritance: 77 | 78 | 79 | Generic Views 80 | ------------- 81 | 82 | .. autoclass:: wildewidgets.views.generic.IndexView 83 | :members: 84 | :undoc-members: 85 | :show-inheritance: 86 | 87 | .. autoclass:: wildewidgets.views.generic.TableAJAXView 88 | :members: 89 | :undoc-members: 90 | :show-inheritance: 91 | 92 | .. autoclass:: wildewidgets.views.generic.TableBulkActionView 93 | :members: 94 | :undoc-members: 95 | :show-inheritance: 96 | 97 | .. autoclass:: wildewidgets.views.generic.CreateView 98 | :members: 99 | :undoc-members: 100 | :show-inheritance: 101 | 102 | .. autoclass:: wildewidgets.views.generic.UpdateView 103 | :members: 104 | :undoc-members: 105 | :show-inheritance: 106 | 107 | .. autoclass:: wildewidgets.views.generic.DeleteView 108 | :members: 109 | :undoc-members: 110 | :show-inheritance: 111 | 112 | 113 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/stackedbarchart_json.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 77 | 78 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/stackedbarchart.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 83 | 84 | -------------------------------------------------------------------------------- /demo/demo/users/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% load static i18n sass_tags %} 2 | 3 | 4 | 5 | 6 | 7 | {# As of June 2018, this is the most up-to-date "responsive design" viewport tag. #} 8 | 9 | 10 | {% block title %}{% endblock %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 36 | 37 | 38 | 39 |
40 |
41 |
42 |
43 | Book Manager Demo 44 |
45 |
46 |
47 | {% csrf_token %} 48 | 49 |
50 | {{ form.username.label_tag}} 51 | {{ form.username }} 52 |
53 |
54 | {{ form.password.label_tag}} 55 | {{ form.password }} 56 |
57 |
58 | 59 |
60 |
61 |
62 |
63 |
64 |
65 | 66 |
67 |
68 | 69 | -------------------------------------------------------------------------------- /demo/demo/users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.2 on 2022-10-25 21:41 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('auth', '0012_alter_user_first_name_max_length'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='User', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('password', models.CharField(max_length=128, verbose_name='password')), 23 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 24 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 25 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 26 | ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), 27 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 28 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 29 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 30 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 31 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 32 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), 33 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), 34 | ], 35 | options={ 36 | 'verbose_name': 'user', 37 | 'verbose_name_plural': 'users', 38 | }, 39 | managers=[ 40 | ('objects', django.contrib.auth.models.UserManager()), 41 | ], 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /wildewidgets/static/wildewidgets/css/_navbar.scss: -------------------------------------------------------------------------------- 1 | .menu .menu-title { 2 | // make the menu titles be fancy 3 | padding: .5rem .75rem; 4 | justify-content: flex-start; 5 | color: #FFFF; 6 | } 7 | 8 | $navbar-expand-breakpoints: ( 9 | xs: 0, 10 | sm: 576px, 11 | md: 768px, 12 | lg: 992px, 13 | xl: 1200px, 14 | xxl: 1400px 15 | ) !default; 16 | 17 | .navbar { 18 | &.navbar-vertical { 19 | // TODO: we'll need to work some of the below out for a horizontal navbar 20 | &.navbar-dark { 21 | .menu { 22 | .menu-title { 23 | background-color: lighten(#1d273b, 10%) !important; 24 | border-bottom: 2px solid lighten(#1d273b, 20%) !important; 25 | } 26 | .nav-subtitle { 27 | font-size: 0.75rem; 28 | background-color: lighten(#1d273b, 2%) !important; 29 | border-bottom: 2px solid lighten(#1d273b, 10%) !important; 30 | border-top: 2px solid lighten(#1d273b, 10%) !important; 31 | } 32 | } 33 | } 34 | 35 | @each $bp_label, $bp_width in $navbar-expand-breakpoints { 36 | &.navbar-wide.navbar-expand-#{"" + $bp_label} { 37 | width: 18rem; 38 | @media screen and (max-width: #{"" + $bp_width}) { 39 | width: auto; 40 | img { 41 | width: auto !important; 42 | height: 5rem; 43 | } 44 | } 45 | } 46 | &.navbar-wide.navbar-expand-#{"" + $bp_label} ~ .page { 47 | padding-left: 18rem; 48 | 49 | @media screen and (max-width: #{"" + $bp_width}) { 50 | padding-left: 0; 51 | } 52 | } 53 | 54 | &.navbar-expand-#{"" + $bp_label} .navbar-collapse { 55 | // .navbar-collapse is the menu area of the .navbar, as 56 | // opposed to the brand area at the top 57 | .nav-item.active, 58 | .nav-item--clickable.active { 59 | // highlight the active .nav-items with a lighter background 60 | background: var(--tblr-navbar-active-bg); 61 | } 62 | .dropdown-menu .dropdown-item { 63 | // make the dropdown-menu blocks be indented a bit 64 | padding-left: 1.5rem; 65 | } 66 | 67 | .menu .nav-link.dropdown-toggle { 68 | &.active { 69 | background: var(--tblr-navbar-active-bg); 70 | } 71 | .nav-link { 72 | font-size: smaller; 73 | &.active { 74 | // highlight active items in a DropdownMenu 75 | background: var(--tblr-navbar-active-bg); 76 | } 77 | } 78 | 79 | } 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/barchart.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 97 | 98 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/menu.html: -------------------------------------------------------------------------------- 1 | 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | _ _ _ _ _ _ _ _ 3 | | (_) (_) | | | (_) | | | | 4 | __| |_ __ _ _ __ __ _ ___ _____ ___| | __| | _____ ___ __| | __ _ ___| |_ ___ 5 | / _` | |/ _` | '_ \ / _` |/ _ \___\ \ /\ / / | |/ _` |/ _ \ \ /\ / / |/ _` |/ _` |/ _ \ __/ __| 6 | | (_| | | (_| | | | | (_| | (_) | \ V V /| | | (_| | __/\ V V /| | (_| | (_| | __/ |_\__ \ 7 | \__,_| |\__,_|_| |_|\__, |\___/ \_/\_/ |_|_|\__,_|\___| \_/\_/ |_|\__,_|\__, |\___|\__|___/ 8 | _/ | __/ | __/ | 9 | |__/ |___/ |___/ 10 | ``` 11 | 12 | `django-wildewidgets` is a Django design library providing several tools for building 13 | full-featured, widget-based web applications with a standard, consistent design, based 14 | on Bootstrap. 15 | 16 | The package includes the source to a [demo](https://wildewidgets.caltech.edu). 17 | 18 | ## Quick start 19 | 20 | Install: 21 | 22 | pip install django-wildewidgets 23 | 24 | If you plan on using [Altair charts](https://github.com/altair-viz/altair), run: 25 | 26 | pip install altair 27 | 28 | Add "wildewidgets" to your INSTALLED_APPS setting like this: 29 | 30 | INSTALLED_APPS = [ 31 | ... 32 | 'wildewidgets', 33 | ] 34 | 35 | 36 | Include the wildewidgets URLconf in your project urls.py like this: 37 | 38 | from wildewidgets import WildewidgetDispatch 39 | 40 | urlpatterns = [ 41 | ... 42 | path('/wildewidgets_json', WildewidgetDispatch.as_view(), name='wildewidgets_json'), 43 | ] 44 | 45 | 46 | Add the appropriate resources to your template files. 47 | 48 | First, add this to your ``: 49 | 50 | 51 | 52 | For [ChartJS](https://www.chartjs.org/) (regular business type charts), add the corresponding javascript file: 53 | 54 | 55 | 56 | For [Altair](https://github.com/altair-viz/altair) (scientific charts), use: 57 | 58 | 59 | 60 | 61 | 62 | For [DataTables](https://github.com/DataTables/DataTables), use: 63 | 64 | 65 | 66 | 67 | 68 | and: 69 | 70 | 71 | 72 | and, if using [Tabler](https://tabler.io), include: 73 | 74 | 75 | 76 | For [ApexCharts](https://apexcharts.com), use: 77 | 78 | 79 | 80 | If you plan on using CodeWidget, you'll need to include the following to get syntax highlighting: 81 | 82 | 83 | 84 | ## Documentation 85 | 86 | [django-wildewidgets.readthedocs.io](http://django-wildewidgets.readthedocs.io/) is the full 87 | reference for django-wildewidgets. 88 | -------------------------------------------------------------------------------- /demo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-slim AS stage-1 2 | 3 | RUN export DEBIAN_FRONTEND=noninteractive && \ 4 | apt-get update && \ 5 | apt-get upgrade -y && \ 6 | apt-get install --yes --no-install-recommends \ 7 | gcc g++ rustc cargo \ 8 | # Some of our python dependencies come from github or gitlab 9 | git \ 10 | # Mysql dependencies for mysqlclient 11 | mariadb-client libmariadb-dev pkg-config \ 12 | # LDAP libraries for python-ldap 13 | libldap-dev libsasl2-dev libldap-common \ 14 | # Various dependencies for common requirements. 15 | libcurl4-openssl-dev libreadline-dev libssl-dev locales-all libffi-dev libxslt1-dev \ 16 | && \ 17 | apt-get clean && \ 18 | ln -sf /bin/bash /bin/sh && \ 19 | /usr/local/bin/pip install --upgrade supervisor pip "setuptools<81" wheel && \ 20 | /usr/local/bin/python -m venv /ve 21 | 22 | COPY requirements.txt /tmp/requirements.txt 23 | RUN /ve/bin/pip install --upgrade pip wheel && \ 24 | /ve/bin/pip install -r /tmp/requirements.txt 25 | 26 | FROM python:3.12-slim AS stage-2 27 | 28 | ENV HISTCONTROL=ignorespace:ignoredups \ 29 | IPYTHONDIR=/etc/ipython \ 30 | LANG=en_US.UTF-8 \ 31 | LANGUAGE=en_US.UTF-8 \ 32 | LC_ALL=en_US.UTF-8 \ 33 | LOGGING_MODE=print \ 34 | # Disable the pip cache to reduce layer size. 35 | PIP_NO_CACHE_DIR=1 \ 36 | PYCURL_SSL_LIBRARY=nss \ 37 | SHELL_PLUS=ipython \ 38 | # This env var overrides other system timezone settings. 39 | TZ=America/Los_Angeles \ 40 | VIRTUAL_ENV=/ve 41 | 42 | RUN export DEBIAN_FRONTEND=noninteractive && \ 43 | apt-get update && \ 44 | apt-get upgrade -y && \ 45 | apt-get install --yes --no-install-recommends \ 46 | nginx \ 47 | mariadb-client libmariadb-dev pkg-config \ 48 | locales-all \ 49 | hostname less make procps psmisc tar telnet vim wget which \ 50 | && \ 51 | apt-get clean && \ 52 | ln -sf /bin/bash /bin/sh && \ 53 | /usr/local/bin/pip install --upgrade supervisor pip "setuptools<81" wheel && \ 54 | adduser --disabled-password app && \ 55 | mkdir -p /var/tmp/nginx/client_body \ 56 | /var/tmp/nginx/proxy \ 57 | /var/tmp/nginx/fastcgi \ 58 | /var/tmp/nginx/uwsgi \ 59 | /var/tmp/nginx/scgi \ 60 | && \ 61 | chown -R app:app /var/tmp/nginx && \ 62 | mkdir -p /certs && \ 63 | openssl req -x509 -nodes \ 64 | -subj "/C=US/ST=CA/O=Caltech/CN=localhost.localdomain" \ 65 | -days 3650 \ 66 | -newkey rsa:2048 \ 67 | -keyout /certs/localhost.key \ 68 | -out /certs/localhost.crt && \ 69 | chown app:app /certs/* 70 | 71 | COPY --from=stage-1 --chown=app:app /ve /ve 72 | ENV PATH=/ve/bin:/app:/usr/local/bin:$PATH 73 | 74 | COPY . /app 75 | WORKDIR /app 76 | 77 | RUN pip install -e . && \ 78 | pip install django-compressor && \ 79 | python manage.py compilescss --settings=demo.settings_docker -v0 --skip-checks && \ 80 | python manage.py collectstatic --settings=demo.settings_docker --noinput -v0 --link && \ 81 | chown -R app:app /static && \ 82 | cp etc/supervisord.conf /etc/supervisord.conf && \ 83 | cp etc/nginx.conf /etc/nginx/nginx.conf && \ 84 | cp etc/gunicorn_logging.conf /etc/gunicorn_logging.conf && \ 85 | mkdir -p /etc/ipython && \ 86 | touch /etc/ipython/__init__.py && \ 87 | cp etc/ipython_config.py /etc/ipython/ipython_config.py && \ 88 | chown -R app:app /etc/ipython 89 | 90 | # Expose the app's communication port. 91 | EXPOSE 8443 92 | 93 | # Switch execution to the app user, so that supervisor runs as app, rather than root. 94 | USER app 95 | 96 | CMD ["/usr/local/bin/supervisord", "-c", "/etc/supervisord.conf"] 97 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Cheetah Demo 2 | 3 | This access.caltech Django application ... 4 | 5 | INSERT REASONABLE DESCRIPTION OF THIS APPLICATION HERE 6 | 7 | ## Operations 8 | 9 | ### Working with the AWS infrastructure for Cheetah Demo 10 | 11 | We're using terraform workspaces, and whatever is the latest version of terraform-0.12. Here's how you set up to work with 12 | the terraform templates in this repository: 13 | 14 | ``` 15 | cd terraform 16 | chtf __LATEST_VERSION__ 17 | terraform init --upgrade 18 | terraform workspace select test 19 | ``` 20 | 21 | Now when you run `terraform plan` and `terraform apply`, you will be working only with the `test` environment. 22 | To work with the prod environment, do 23 | 24 | ``` 25 | terraform workspace select prod 26 | ``` 27 | 28 | To list the available environments, do: 29 | 30 | ``` 31 | terraform workspace list 32 | ``` 33 | 34 | ### Configs for the cloud 35 | 36 | The ADS KeePass has the /etc/context.d .env files needed for running the 37 | `deploy config` commands. They're named for the service, and are under 38 | "deployfish .env files". 39 | 40 | ### Logs for the cloud 41 | 42 | The logs for the test and prod servers end up of course in the ADS ELK stack: 43 | http://ads-logs.cloud.caltech.edu/_plugin/kibana/. They will both have the 44 | "application" set to "cheetah-demo". 45 | 46 | A good way to search Kibana for those all relevant logs for the test server is: 47 | 48 | ``` 49 | application:"cheetah-demo" AND environment:test AND NOT message:HealthChecker 50 | ``` 51 | 52 | A good way to search Kibana for those all relevant logs for the prod server is: 53 | 54 | ``` 55 | application:"cheetah-demo" AND environment:prod AND NOT message:HealthChecker 56 | ``` 57 | 58 | Thes both say "give me the logs from our service but leave out all the spam from 59 | the ALB running its health checks on the service." 60 | 61 | ## Contributing to the code of Cheetah Demo 62 | 63 | ## Setup your local virtualenv 64 | 65 | The Amazon Linux 2 base image we use here has Python 3.7.6, so we'll want that in our virtualenv. 66 | 67 | ``` 68 | git clone git@bitbucket.org:caltech-imss-ads/demo.git 69 | cd demo 70 | pyenv virtualenv 3.7.6 demo 71 | pyenv local demo 72 | pip install --upgrade pip 73 | pip install -r requirements.txt 74 | ``` 75 | 76 | If you don't have a `pyenv` python 3.7.6 built, build it like so: 77 | 78 | ``` 79 | pyenv install 3.7.6 80 | ``` 81 | 82 | ### Prepare the docker environment 83 | 84 | Now copy in the Docker environment file to the appropriate place on your Mac: 85 | 86 | ``` 87 | cp etc/environment.txt /etc/context.d/demo.env 88 | ``` 89 | 90 | Edit `/etc/context.d/demo.env` and set the following things: 91 | 92 | * `AWS_ACCESS_KEY_ID`: set this to your own `AWS_ACCESS_KEY_ID` 93 | * `AWS_SECRET_ACCESS_KEY`: set this to your own `AWS_SECRET_ACCESS_KEY` 94 | 95 | ### Build the Docker image 96 | 97 | ``` 98 | make build 99 | ``` 100 | 101 | ### Run the service, and initialize the databse 102 | 103 | ``` 104 | make dev-detached 105 | make exec 106 | > ./manage.py migrate 107 | ``` 108 | 109 | ### Getting to the service in your browser 110 | 111 | Since Cheetah Demo is meant to run behind the access.caltech proxy servers, you'll need to supply the 112 | access.caltech HTTP Request headers in order for it to work correctly. You'll need to use something 113 | like Firefox's Modify Headers or Chrome's [ModHeader](https://bewisse.com/modheader/) plugin so that you can set the appropriate HTTP Headers. 114 | 115 | Set the following Request headers: 116 | 117 | * `User` to your access.caltech username 118 | * `SM_USER` to your access.caltech username 119 | * `CAPCaltechUID` to your Caltech UID, 120 | * `user_mail` to your e-mail address 121 | * `user_first_name` to your first name 122 | * `user_last_name` to your last name 123 | 124 | You should how be able to browse to https://localhost:8062/demo . 125 | -------------------------------------------------------------------------------- /demo/etc/nginx.conf: -------------------------------------------------------------------------------- 1 | error_log /dev/stderr info; 2 | pid /tmp/nginx.pid; 3 | daemon off; 4 | 5 | events { 6 | worker_connections 1024; 7 | } 8 | 9 | http { 10 | include /etc/nginx/mime.types; 11 | default_type application/octet-stream; 12 | 13 | # Enable on-the-fly gzip compression of HTML, javascript, CSS, plaintext, and xml. 14 | gzip on; 15 | gzip_vary on; 16 | gzip_min_length 1024; 17 | gzip_proxied expired no-cache no-store private auth; 18 | gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml; 19 | 20 | # Use a more fine-grained timestamp: include milliseconds so we can order 21 | # requests properly 22 | map "$time_iso8601 # $msec" $time_iso8601_ms { 23 | "~(^.+)-0[78]:00 # \d+\.(\d+)$" $1,$2; 24 | } 25 | log_format json_combined escape=json 26 | '{' 27 | '"type":"access",' 28 | '"program":"nginx",' 29 | '"time_local":"$time_iso8601_ms",' 30 | '"remote_addr":"$http_x_forwarded_for",' 31 | '"remote_user":"$http_user",' 32 | '"request":"$request",' 33 | '"status":"$status",' 34 | '"method":"$request_method",' 35 | '"path":"$uri",' 36 | '"response_length":"$body_bytes_sent",' 37 | '"request_time":"$request_time",' 38 | '"http_referrer":"$http_referer",' 39 | '"http_user_agent":"$http_user_agent",' 40 | '"host":"$http_host"' 41 | '}'; 42 | access_log /dev/stdout json_combined; 43 | 44 | # Improves the performance of serving static files, which is most of what 45 | # nginx does for us, so performance is key. 46 | sendfile on; 47 | tcp_nopush on; 48 | 49 | client_body_temp_path /var/tmp/nginx/client_body; 50 | proxy_temp_path /var/tmp/nginx/proxy; 51 | fastcgi_temp_path /var/tmp/nginx/fastcgi; 52 | uwsgi_temp_path /var/tmp/nginx/uwsgi; 53 | scgi_temp_path /var/tmp/nginx/scgi; 54 | 55 | server { 56 | listen 8443 ssl http2; 57 | 58 | location ^~ /static/demo/ { 59 | gzip_static on; 60 | expires max; 61 | add_header Cache-Control public; # nosemgrep 62 | alias /static/; 63 | } 64 | 65 | server_name localhost; 66 | 67 | ######## SECURITY CONFIGURATION ######## 68 | 69 | add_header Strict-Transport-Security "max-age=63072000"; 70 | add_header X-XSS-Protection "1; mode=block"; 71 | 72 | ############# SSL CONFIGS ############## 73 | 74 | ssl_certificate /certs/localhost.crt; 75 | ssl_certificate_key /certs/localhost.key; 76 | ssl_verify_client off; 77 | ssl_session_cache shared:SSL:50m; 78 | ssl_session_timeout 1d; 79 | ssl_session_tickets on; 80 | 81 | ######## GENERIC server CONFIGS ######## 82 | 83 | server_name localhost; 84 | client_max_body_size 100M; 85 | client_header_timeout 305s; 86 | client_body_timeout 305s; 87 | keepalive_timeout 305s; 88 | 89 | if ($request_method ~ ^(TRACE|TRACK)$ ) { 90 | return 405; 91 | } 92 | 93 | underscores_in_headers on; 94 | server_tokens off; 95 | 96 | # nginx stats location 97 | location = /server-status { 98 | stub_status; 99 | access_log off; 100 | log_not_found off; 101 | } 102 | 103 | 104 | # Create the view that the ALB Target Group health check will look at. 105 | location = /lb-status { 106 | return 200 'Hello, Mr. Load balancer.'; 107 | add_header Content-Type text/plain; # nosemgrep 108 | } 109 | 110 | location = /favicon.ico { 111 | access_log off; 112 | log_not_found off; 113 | } 114 | 115 | location / { 116 | proxy_pass http://unix:/tmp/demo.sock; 117 | proxy_set_header Host $http_host; 118 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 119 | proxy_set_header X-Forwarded_Proto $scheme; 120 | proxy_http_version 1.1; 121 | proxy_intercept_errors on; 122 | proxy_redirect off; 123 | proxy_read_timeout 305s; 124 | proxy_connect_timeout 305s; 125 | proxy_send_timeout 305s; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /docs/widgets.rst: -------------------------------------------------------------------------------- 1 | ******* 2 | Widgets 3 | ******* 4 | 5 | There are a number of general purpose widgets available, along with some supporting classes. 6 | 7 | * BasicMenu 8 | * LightMenu - Often used as a submenu beneath the main menu. 9 | * MenuMixin - Used for view classes that utilize menus. 10 | * TemplateWidget - A generic widget that gives you full control over both the content and the layout. 11 | * TabbedWidget - A widget that contains other widgets in a tabbed interface. 12 | * BasicHeader - A header widget that is a base class for widgets with right justified controls. 13 | * HeaderWithLinkButton - A header widget with a link button on the right. 14 | * HeaderWithModalButton - A header widget with a modal button on the right. 15 | * ModalWidget - A Bootstrap modal dialog widget base class. 16 | * CrispyFormModalWidget - A Boostrap modal dialog containing a crispy form. 17 | * WidgetStream - A container widget that contains a list of child widgets that are displayed sequentially. 18 | * CardWidget - A Bootstrap card widget that displays a child widget in its body. 19 | * CodeWidget - A widget that contains a block of syntax highlighted code. 20 | * MarkdownWidget - A widget that contains a block of rendered markdown text. 21 | 22 | Menu 23 | ==== 24 | 25 | A basic menu requires only one class variable defined, `items`:: 26 | 27 | class MainMenu(BasicMenu): 28 | 29 | items = [ 30 | ('Users', 'core:home'), 31 | ('Uploads','core:uploads'), 32 | ] 33 | 34 | The `items` variable is a list of tuples, where the first element is the menu item text and the second element is the URL name. If the `items` variable is defined dynamically in `__init__`, a third optional element in the tuple is a dictionary of get arguments. 35 | 36 | View Mixin 37 | ---------- 38 | 39 | The view mixin `MenuMixin` only requires you to specify the menu class, and the name of the menu item that should be selected:: 40 | 41 | class TestView(MenuMixin, TemplateView): 42 | menu_class = MainMenu 43 | menu_item = 'Users' 44 | ... 45 | 46 | If several views use the same menu, you can create a subclass:: 47 | 48 | class UsersMenuMixin(MenuMixin): 49 | menu_class = MainMenu 50 | menu_item = 'Users' 51 | 52 | Then the view won't need to define these variables:: 53 | 54 | class TestView(UsersMenuMixin, TemplateView): 55 | ... 56 | 57 | Sub Menus 58 | --------- 59 | 60 | Typically, a `LightMenu`` is used as a submenu, below the main menu. The view class, or menu mixin, then becomes:: 61 | 62 | class TestView(MenuMixin, TemplateView): 63 | menu_class = MainMenu 64 | menu_item = 'Users' 65 | submenu_class = SubMenu 66 | submenu_item = 'Main User Task' 67 | ... 68 | 69 | TemplateWidget 70 | ============== 71 | 72 | A template widget encapsulates a defined UI element on a page. It consists of data, and the template to display the data:: 73 | 74 | class HelloWorldWidget(TemplateWidget): 75 | template_name = 'core/hello_world.html' 76 | 77 | def get_context_data(self, **kwargs): 78 | kwargs['data'] = "Hello world" 79 | return kwargs 80 | 81 | TabbedWidget 82 | ============ 83 | 84 | A tabbed widget contains other widgets in a tabbed interface. Tabs are added by called `add_tab` with the name to display on the tab, and the widget to display under that tab. It can be any type of wildewidgets widget:: 85 | 86 | class TestTabbedWidget(TabbedWidget): 87 | 88 | def __init__(self, *args, **kwargs): 89 | super().__init__(*args, **kwargs) 90 | widgets = WidgetStream() 91 | widgets.add_widget(Test1Header()) 92 | widgets.add_widget(Test1Table()) 93 | self.add_tab("Test 1", widgets) 94 | 95 | widgets = WidgetStream() 96 | widgets.add_widget(Test2Header()) 97 | widgets.add_widget(Test2Table()) 98 | self.add_tab("Test 2", widgets) 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | django-wildewidgets is a Django library designed to help you make charts, graphs, tables, and UI widgets 5 | quickly and easily with libraries like Chartjs, Altair, and Datatables. 6 | 7 | Install 8 | ------- 9 | 10 | :: 11 | 12 | pip install django-wildewidgets 13 | 14 | If you plan on using `Altair charts `_, run:: 15 | 16 | pip install altair 17 | 18 | If you plan on using the Markdown Widget, install `django-markdownify `_:: 19 | 20 | pip install django-markdownify 21 | 22 | Configure 23 | --------- 24 | 25 | Add "wildewidgets" to your INSTALLED_APPS setting like this:: 26 | 27 | INSTALLED_APPS = [ 28 | ... 29 | 'wildewidgets', 30 | ] 31 | 32 | Include the wildewidgets URLconf in your project urls.py like this:: 33 | 34 | from wildewidgets import WildewidgetDispatch 35 | 36 | urlpatterns = [ 37 | ... 38 | path('/wildewidgets_json', WildewidgetDispatch.as_view(), name='wildewidgets_json'), 39 | ] 40 | 41 | If you plan on using the Markdown Widget, add `markdownify` to your `INSTALLED_APPS`:: 42 | 43 | INSTALLED_APPS = [ 44 | ... 45 | 'markdownify', 46 | ] 47 | 48 | and optionally configure it in your `settings.py`:: 49 | 50 | MARKDOWNIFY = { 51 | "default": { 52 | "WHITELIST_TAGS": bleach.sanitizer.ALLOWED_TAGS + ["p", "h1", "h2"] 53 | }, 54 | } 55 | 56 | Static Resources 57 | ---------------- 58 | 59 | Add the appropriate resources to your template files. 60 | 61 | If using `WidgetListLayout`, add the following to your template:: 62 | 63 | {% static 'wildewidgets/css/wildewidgets.css' %} 64 | 65 | For `ChartJS `_ (regular business type charts), add the corresponding javascript file:: 66 | 67 | 68 | 69 | For `Altair `_ (scientific charts), use:: 70 | 71 | 72 | 73 | 74 | 75 | For `DataTables `_, use:: 76 | 77 | 78 | 79 | 80 | 81 | and:: 82 | 83 | 84 | 85 | If you want to add the export buttons to a DataTable, also add:: 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | and, if using `Tabler `_, include:: 96 | 97 | 98 | 99 | For `ApexCharts `_, use:: 100 | 101 | 102 | 103 | If you plan on using `CodeWidget`, you'll need to include the following to get syntax highlighting:: 104 | 105 | 106 | -------------------------------------------------------------------------------- /demo/bin/wait-for-it.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Use this script to test if a given TCP host/port are available 3 | 4 | cmdname=$(basename $0) 5 | 6 | echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } 7 | 8 | usage() 9 | { 10 | cat << USAGE >&2 11 | Usage: 12 | $cmdname host:port [-s] [-t timeout] [-- command args] 13 | -h HOST | --host=HOST Host or IP under test 14 | -p PORT | --port=PORT TCP port under test 15 | Alternatively, you specify the host and port as host:port 16 | -s | --strict Only execute subcommand if the test succeeds 17 | -q | --quiet Don't output any status messages 18 | -t TIMEOUT | --timeout=TIMEOUT 19 | Timeout in seconds, zero for no timeout 20 | -- COMMAND ARGS Execute command with args after the test finishes 21 | USAGE 22 | exit 1 23 | } 24 | 25 | wait_for() 26 | { 27 | if [[ $TIMEOUT -gt 0 ]]; then 28 | echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT" 29 | else 30 | echoerr "$cmdname: waiting for $HOST:$PORT without a timeout" 31 | fi 32 | start_ts=$(date +%s) 33 | while : 34 | do 35 | (echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1 36 | result=$? 37 | if [[ $result -eq 0 ]]; then 38 | end_ts=$(date +%s) 39 | echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds" 40 | break 41 | fi 42 | sleep 1 43 | done 44 | return $result 45 | } 46 | 47 | wait_for_wrapper() 48 | { 49 | # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 50 | if [[ $QUIET -eq 1 ]]; then 51 | timeout $TIMEOUT $0 --quiet --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & 52 | else 53 | timeout $TIMEOUT $0 --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & 54 | fi 55 | PID=$! 56 | trap "kill -INT -$PID" INT 57 | wait $PID 58 | RESULT=$? 59 | if [[ $RESULT -ne 0 ]]; then 60 | echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT" 61 | fi 62 | return $RESULT 63 | } 64 | 65 | # process arguments 66 | while [[ $# -gt 0 ]] 67 | do 68 | case "$1" in 69 | *:* ) 70 | hostport=(${1//:/ }) 71 | HOST=${hostport[0]} 72 | PORT=${hostport[1]} 73 | shift 1 74 | ;; 75 | --child) 76 | CHILD=1 77 | shift 1 78 | ;; 79 | -q | --quiet) 80 | QUIET=1 81 | shift 1 82 | ;; 83 | -s | --strict) 84 | STRICT=1 85 | shift 1 86 | ;; 87 | -h) 88 | HOST="$2" 89 | if [[ $HOST == "" ]]; then break; fi 90 | shift 2 91 | ;; 92 | --host=*) 93 | HOST="${1#*=}" 94 | shift 1 95 | ;; 96 | -p) 97 | PORT="$2" 98 | if [[ $PORT == "" ]]; then break; fi 99 | shift 2 100 | ;; 101 | --port=*) 102 | PORT="${1#*=}" 103 | shift 1 104 | ;; 105 | -t) 106 | TIMEOUT="$2" 107 | if [[ $TIMEOUT == "" ]]; then break; fi 108 | shift 2 109 | ;; 110 | --timeout=*) 111 | TIMEOUT="${1#*=}" 112 | shift 1 113 | ;; 114 | --and) 115 | shift 116 | CLI="$@" 117 | break 118 | ;; 119 | --help) 120 | usage 121 | ;; 122 | *) 123 | echoerr "Unknown argument: $1" 124 | usage 125 | ;; 126 | esac 127 | done 128 | 129 | if [[ "$HOST" == "" || "$PORT" == "" ]]; then 130 | echoerr "Error: you need to provide a host and port to test." 131 | usage 132 | fi 133 | 134 | TIMEOUT=${TIMEOUT:-15} 135 | STRICT=${STRICT:-0} 136 | CHILD=${CHILD:-0} 137 | QUIET=${QUIET:-0} 138 | 139 | if [[ $CHILD -gt 0 ]]; then 140 | wait_for 141 | RESULT=$? 142 | exit $RESULT 143 | else 144 | if [[ $TIMEOUT -gt 0 ]]; then 145 | wait_for_wrapper 146 | RESULT=$? 147 | else 148 | wait_for 149 | RESULT=$? 150 | fi 151 | fi 152 | 153 | if [[ $CLI != "" ]]; then 154 | if [[ $RESULT -ne 0 && $STRICT -eq 1 ]]; then 155 | echoerr "$cmdname: strict mode, refusing to execute subprocess" 156 | exit $RESULT 157 | fi 158 | exec $CLI 159 | else 160 | exit $RESULT 161 | fi 162 | -------------------------------------------------------------------------------- /demo/requirements.txt: -------------------------------------------------------------------------------- 1 | # Django and third-party apps 2 | # ------------------------------------------------------------------------------ 3 | # When you update django in this file, update the links in settings.py, too! 4 | Django==5.2.4 # https://www.djangoproject.com/ 5 | django-autocomplete-light==3.5.1 # https://github.com/yourlabs/django-autocomplete-light 6 | django-braces==1.14.0 # https://github.com/brack3t/django-braces 7 | django-crequest==2018.5.11 # https://github.com/Alir3z4/django-crequest 8 | django-crispy-forms==2.4 # https://github.com/django-crispy-forms/django-crispy-forms 9 | crispy-bootstrap5==2025.6 # https://github.com/django-crispy-forms/crispy-bootstrap5 10 | django-environ==0.4.5 # https://github.com/joke2k/django-environ 11 | django-js-reverse==0.9.1 # https://github.com/ierror/django-js-reverse 12 | django-markdownify==0.9.2 # https://github.com/erwinmatijsen/django-markdownify 13 | django-redis==4.11.0 # https://github.com/jazzband/django-redis 14 | django-storages==1.14.4 # https://github.com/jschneier/django-storages 15 | django-xff==1.3.0 # https://github.com/ferrix/xff/ 16 | django-chartjs==2.2.1 # https://github.com/peopledoc/django-chartjs 17 | django-theme-academy==0.3.2 # https://github.com/caltechads/django-theme-academy 18 | django-book-manager==0.3.2 # https://github.com/caltechads/django-book-manager 19 | django-wildewidgets==1.2.4 # https://github.com/caltechads/django-wildewidgets 20 | 21 | # Other utils 22 | # ------------------------------------------------------------------------------ 23 | bleach==5.0.1 # https://github.com/mozilla/bleach 24 | altair==5.5.0 # https://github.com/altair-viz/altair 25 | pandas==2.3.1 # https://github.com/pandas-dev/pandas 26 | colorama==0.4.6 # https://github.com/tartley/colorama 27 | crython==0.2.0 # https://github.com/ahawker/crython 28 | ipython==8.6.0 # https://github.com/ipython/ipython 29 | mysqlclient==2.1.1 # https://github.com/PyMySQL/mysqlclient-python 30 | pytz==2022.6 # https://github.com/stub42/pytz 31 | structlog==22.2.0 # https://github.com/hynek/structlog 32 | # --- SASS Processing 33 | django-sass-processor==1.4.1 # https://github.com/jrief/django-sass-processor 34 | libsass==0.22.0 35 | 36 | # Web server 37 | # ------------------------------------------------------------------------------ 38 | gunicorn==20.1.0 # https://github.com/benoitc/gunicorn 39 | 40 | 41 | # Deployment 42 | # ------------------------------------------------------------------------------ 43 | bumpversion==0.6.0 # https://github.com/peritus/bumpversion 44 | 45 | 46 | # # Development 47 | # # ------------------------------------------------------------------------------ 48 | # django-debug-toolbar==2.2 # https://github.com/jazzband/django-debug-toolbar 49 | # django-debug-toolbar-template-profiler==2.0.1 # https://github.com/node13h/django-debug-toolbar-template-profiler 50 | # django-queryinspect==1.1.0 # https://github.com/dobarkod/django-queryinspect 51 | # django-extensions==2.2.9 # https://github.com/django-extensions/django-extensions 52 | # autopep8==1.5 # https://github.com/hhatto/autopep8 53 | # flake8==3.7.9 # https://github.com/PyCQA/flake8 54 | # pycodestyle==2.5.0 # https://github.com/PyCQA/pycodestyle 55 | 56 | # # Testing 57 | # # ------------------------------------------------------------------------------ 58 | # coverage==5.0.3 # https://github.com/nedbat/coveragepy 59 | # django-coverage-plugin==1.8.0 # https://github.com/nedbat/django_coverage_plugin 60 | # factory-boy==2.12.0 # https://github.com/FactoryBoy/factory_boy 61 | # mypy==0.701 # https://github.com/python/mypy 62 | # testfixtures==6.14.0 # https://github.com/Simplistix/testfixtures 63 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | # import sphinx_rtd_theme 17 | 18 | import django 19 | import os 20 | import sys 21 | 22 | sys.path.insert(0, os.path.abspath("../demo")) 23 | os.environ["DJANGO_SETTINGS_MODULE"] = "demo.settings" 24 | django.setup() 25 | 26 | 27 | # -- Project information ----------------------------------------------------- 28 | 29 | # the master toctree document 30 | master_doc = "index" 31 | 32 | project = "django-wildewidgets" 33 | copyright = "2023, California Institute of Technology" # pylint: disable=redefined-builtin 34 | author = "Glenn Bach, Chris Malek" 35 | 36 | from typing import Dict, Tuple, Optional # noqa: E402 37 | 38 | 39 | # The full version, including alpha/beta/rc tags 40 | release = "1.2.4" 41 | 42 | 43 | # -- General configuration --------------------------------------------------- 44 | 45 | # Add any Sphinx extension module names here, as strings. They can be 46 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 47 | # ones. 48 | extensions = [ 49 | "sphinx.ext.napoleon", 50 | "sphinx.ext.autodoc", 51 | "sphinx.ext.viewcode", 52 | "sphinxcontrib.images", 53 | "sphinx.ext.intersphinx", 54 | ] 55 | 56 | 57 | # Add any paths that contain templates here, relative to this directory. 58 | templates_path = ["_templates"] 59 | 60 | # List of patterns, relative to source directory, that match files and 61 | # directories to ignore when looking for source files. 62 | # This pattern also affects html_static_path and html_extra_path. 63 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 64 | 65 | add_function_parentheses: bool = False 66 | add_module_names: bool = False 67 | 68 | autodoc_member_order = "groupwise" 69 | 70 | # Make Sphinx not expand all our Type Aliases 71 | autodoc_type_aliases: Dict[str, str] = {} 72 | 73 | # the locations and names of other projects that should be linked to this one 74 | intersphinx_mapping: Dict[str, Tuple[str, Optional[str]]] = { 75 | "python": ("https://docs.python.org/3", None), 76 | "django": ( 77 | "http://docs.djangoproject.com/en/dev/", 78 | "http://docs.djangoproject.com/en/dev/_objects/", 79 | ), 80 | } 81 | 82 | # Configure the path to the Django settings module 83 | django_settings = "demo.settings_docker" 84 | # Include the database table names of Django models 85 | django_show_db_tables = True 86 | 87 | # -- Options for HTML output ------------------------------------------------- 88 | 89 | # The theme to use for HTML and HTML Help pages. See the documentation for 90 | # a list of builtin themes. 91 | # 92 | html_theme = "pydata_sphinx_theme" 93 | # html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 94 | html_context = { 95 | "display_github": True, # Integrate github 96 | "github_user": "caltech-imss-ads", # Username 97 | "github_repo": "django-wildewidgets", # Repo name 98 | "github_version": "main", # Version 99 | "conf_py_path": "/docs/", # Path in the checkout to the docs root 100 | } 101 | 102 | # Theme options are theme-specific and customize the look and feel of a theme 103 | # further. For a list of options available for each theme, see the 104 | # documentation. 105 | # 106 | html_theme_options = { 107 | "collapse_navigation": True, 108 | "navigation_depth": 3, 109 | "show_prev_next": False, 110 | "logo": { 111 | "image_light": "wildewidgets_logo.png", 112 | "image_dark": "wildewidgets_dark_mode_logo.png", 113 | "text": "Django-Wildewidgets", 114 | }, 115 | "icon_links": [ 116 | { 117 | "name": "GitHub", 118 | "url": "https://github.com/caltechads/django-wildewidgets", 119 | "icon": "fab fa-github-square", 120 | "type": "fontawesome", 121 | }, 122 | { 123 | "name": "Demo", 124 | "url": "https://wildewidgets.caltech.edu", 125 | "icon": "fa fa-desktop", 126 | "type": "fontawesome", 127 | }, 128 | ], 129 | } 130 | # Add any paths that contain custom static files (such as style sheets) here, 131 | # relative to this directory. They are copied after the builtin static files, 132 | # so a file named "default.css" will overwrite the builtin "default.css". 133 | html_static_path = ["_static"] 134 | html_logo = "_static/wildewidgets.png" 135 | html_favicon = "_static/favicon.ico" 136 | -------------------------------------------------------------------------------- /wildewidgets/widgets/icons.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | 5 | from django.core.exceptions import ImproperlyConfigured 6 | 7 | from .base import Block 8 | 9 | 10 | class FontIcon(Block): 11 | """ 12 | Render a font-based Bootstrap icon, for example: 13 | 14 | .. code-block:: html 15 | 16 | 17 | 18 | See the `Boostrap Icons `_ list for the 19 | list of icons. Find an icon you like, and use the name of that icon on that 20 | page as the ``icon`` kwarg to the constructor, or set it as the :py:attr:`icon` 21 | class variable. 22 | 23 | Example: 24 | .. code-block:: python 25 | 26 | from wildewidgets import FontIcon 27 | 28 | icon = FontIcon(icon="star") 29 | 30 | Keyword Args: 31 | icon: the name of the icon to render, from the Bootstrap Icons list 32 | color: use this as Tabler color name to use as the foreground 33 | font color, leaving the background transparent. If ``background`` 34 | is also set, this is ignored. Look at `Tabler: Colors 35 | `_ 36 | for your choices; set this to the text after the ``bg-`` 37 | background: use this as Tabler background/foreground color set for 38 | this icon. : This overrides :py:attr:`color`. Look 39 | at `Tabler: Colors `_ 40 | for your choices; set this to the text after the ``bg-`` 41 | 42 | """ 43 | 44 | tag: str = "i" 45 | block: str = "fonticon" 46 | 47 | #: The icon font family prefix. One could override this to use FontAwesome icons, 48 | #: for instance, buy changing it to ``fa`` 49 | prefix: str = "bi" 50 | 51 | #: Use this as the name for the icon to render 52 | icon: str 53 | #: If not ``None``, use this as Tabler color name to use as the foreground 54 | #: font color, leaving the background transparent. If :py:attr:`background` 55 | #: is also set, this is ignored. Look at `Tabler: Colors 56 | # `_ for your choices; set 57 | #: this to the text after the ``bg-`` 58 | color: str | None = None 59 | #: If not ``None``, use this as Tabler background/foreground color set for 60 | #: this icon. : This overrides :py:attr:`color`. Look 61 | #: at `Tabler: Colors `_ 62 | #: for your choices; set this to the text after the ``bg-`` 63 | background: str | None = None 64 | 65 | def __init__( 66 | self, 67 | icon: str | None = None, 68 | color: str | None = None, 69 | background: str | None = None, 70 | **kwargs: Any, 71 | ) -> None: 72 | self.icon = icon if icon else self.icon 73 | if not self.icon: 74 | # If icon is not set, we can't render this widget 75 | msg = "icon must be defined as a keyword argument or class attribute" 76 | raise ImproperlyConfigured(msg) 77 | if not isinstance(self.icon, str): 78 | # If icon is not a string, we can't render this widget 79 | msg = f"icon must be a string, not {type(self.icon).__name__}" 80 | raise ImproperlyConfigured(msg) 81 | super().__init__(**kwargs) 82 | self.color = color if color else self.color 83 | self.background = background if background else self.background 84 | self.icon = f"{self.prefix}-{icon}" 85 | self.add_class(self.icon) 86 | if self.color: 87 | self.add_class(f"text-{self.color} bg-transparent") 88 | elif self.background: 89 | self.add_class(f" bg-{self.background} text-{self.background}-fg") 90 | 91 | 92 | class TablerFontIcon(FontIcon): 93 | """ 94 | :py:class:`FontIcon` for Tabler Icons. 95 | 96 | You must include the Tabler Icons CSS in your HTML template: 97 | 98 | .. code-block:: html 99 | 100 | 102 | 103 | Example: 104 | .. code-block:: python 105 | 106 | from wildewidgets import TablerFontIcon 107 | 108 | icon = TablerFontIcon(icon="star") 109 | 110 | """ 111 | 112 | prefix: str = "ti ti" 113 | 114 | 115 | class TablerMenuIcon(FontIcon): 116 | """ 117 | A Tabler menu specific icon. This just adds some menu specific classes and 118 | uses a ```` instead of a ````. It is used by 119 | :py:class:`wildewidgets.NavItem`, :py:class:`wildewidgets.NavDropdownItem` 120 | and :py:class:`wildewidgets.DropdownItem` objects. 121 | 122 | Typically, you won't use this directly, but instead it will be created for 123 | you from a :py:class:`wildewdigets.MenuItem` specification when 124 | :py:class:`wildewdigets.MenuItem.icon` is not ``None``. 125 | 126 | Example: 127 | .. code-block:: python 128 | 129 | from wildewidgets import NavItem, DropdownItem, NavDropdownItem 130 | from wildewidgets import TablerMenuIcon 131 | 132 | icon = TablerMenuIcon(icon='target') 133 | item = NavItem(text='Page', url='/page', icon=icon) 134 | item2 = DropdownItem(text='Page', url='/page', icon=icon) 135 | item3 = NavDropdownItem(text='Page', url='/page', icon=icon) 136 | 137 | """ 138 | 139 | tag: str = "span" 140 | block: str = "nav-link-icon" 141 | css_class: str = "d-md-none d-lg-inline-block" 142 | -------------------------------------------------------------------------------- /wildewidgets/static/wildewidgets/css/highlighting.css: -------------------------------------------------------------------------------- 1 | pre { line-height: 125%; } 2 | td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } 3 | span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } 4 | td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } 5 | span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } 6 | .wildewidgets_highlight .hll { background-color: #ffffcc } 7 | .wildewidgets_highlight { background: #f8f8f8; } 8 | .wildewidgets_highlight .c { color: #408080; font-style: italic } /* Comment */ 9 | .wildewidgets_highlight .err { border: 1px solid #FF0000 } /* Error */ 10 | .wildewidgets_highlight .k { color: #008000; font-weight: bold } /* Keyword */ 11 | .wildewidgets_highlight .o { color: #666666 } /* Operator */ 12 | .wildewidgets_highlight .ch { color: #408080; font-style: italic } /* Comment.Hashbang */ 13 | .wildewidgets_highlight .cm { color: #408080; font-style: italic } /* Comment.Multiline */ 14 | .wildewidgets_highlight .cp { color: #BC7A00 } /* Comment.Preproc */ 15 | .wildewidgets_highlight .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */ 16 | .wildewidgets_highlight .c1 { color: #408080; font-style: italic } /* Comment.Single */ 17 | .wildewidgets_highlight .cs { color: #408080; font-style: italic } /* Comment.Special */ 18 | .wildewidgets_highlight .gd { color: #A00000 } /* Generic.Deleted */ 19 | .wildewidgets_highlight .ge { font-style: italic } /* Generic.Emph */ 20 | .wildewidgets_highlight .gr { color: #FF0000 } /* Generic.Error */ 21 | .wildewidgets_highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 22 | .wildewidgets_highlight .gi { color: #00A000 } /* Generic.Inserted */ 23 | .wildewidgets_highlight .go { color: #888888 } /* Generic.Output */ 24 | .wildewidgets_highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ 25 | .wildewidgets_highlight .gs { font-weight: bold } /* Generic.Strong */ 26 | .wildewidgets_highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 27 | .wildewidgets_highlight .gt { color: #0044DD } /* Generic.Traceback */ 28 | .wildewidgets_highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ 29 | .wildewidgets_highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ 30 | .wildewidgets_highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ 31 | .wildewidgets_highlight .kp { color: #008000 } /* Keyword.Pseudo */ 32 | .wildewidgets_highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ 33 | .wildewidgets_highlight .kt { color: #B00040 } /* Keyword.Type */ 34 | .wildewidgets_highlight .m { color: #666666 } /* Literal.Number */ 35 | .wildewidgets_highlight .s { color: #BA2121 } /* Literal.String */ 36 | .wildewidgets_highlight .na { color: #7D9029 } /* Name.Attribute */ 37 | .wildewidgets_highlight .nb { color: #008000 } /* Name.Builtin */ 38 | .wildewidgets_highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */ 39 | .wildewidgets_highlight .no { color: #880000 } /* Name.Constant */ 40 | .wildewidgets_highlight .nd { color: #AA22FF } /* Name.Decorator */ 41 | .wildewidgets_highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */ 42 | .wildewidgets_highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ 43 | .wildewidgets_highlight .nf { color: #0000FF } /* Name.Function */ 44 | .wildewidgets_highlight .nl { color: #A0A000 } /* Name.Label */ 45 | .wildewidgets_highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ 46 | .wildewidgets_highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ 47 | .wildewidgets_highlight .nv { color: #19177C } /* Name.Variable */ 48 | .wildewidgets_highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ 49 | .wildewidgets_highlight .w { color: #bbbbbb } /* Text.Whitespace */ 50 | .wildewidgets_highlight .mb { color: #666666 } /* Literal.Number.Bin */ 51 | .wildewidgets_highlight .mf { color: #666666 } /* Literal.Number.Float */ 52 | .wildewidgets_highlight .mh { color: #666666 } /* Literal.Number.Hex */ 53 | .wildewidgets_highlight .mi { color: #666666 } /* Literal.Number.Integer */ 54 | .wildewidgets_highlight .mo { color: #666666 } /* Literal.Number.Oct */ 55 | .wildewidgets_highlight .sa { color: #BA2121 } /* Literal.String.Affix */ 56 | .wildewidgets_highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ 57 | .wildewidgets_highlight .sc { color: #BA2121 } /* Literal.String.Char */ 58 | .wildewidgets_highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ 59 | .wildewidgets_highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ 60 | .wildewidgets_highlight .s2 { color: #BA2121 } /* Literal.String.Double */ 61 | .wildewidgets_highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ 62 | .wildewidgets_highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ 63 | .wildewidgets_highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ 64 | .wildewidgets_highlight .sx { color: #008000 } /* Literal.String.Other */ 65 | .wildewidgets_highlight .sr { color: #BB6688 } /* Literal.String.Regex */ 66 | .wildewidgets_highlight .s1 { color: #BA2121 } /* Literal.String.Single */ 67 | .wildewidgets_highlight .ss { color: #19177C } /* Literal.String.Symbol */ 68 | .wildewidgets_highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ 69 | .wildewidgets_highlight .fm { color: #0000FF } /* Name.Function.Magic */ 70 | .wildewidgets_highlight .vc { color: #19177C } /* Name.Variable.Class */ 71 | .wildewidgets_highlight .vg { color: #19177C } /* Name.Variable.Global */ 72 | .wildewidgets_highlight .vi { color: #19177C } /* Name.Variable.Instance */ 73 | .wildewidgets_highlight .vm { color: #19177C } /* Name.Variable.Magic */ 74 | .wildewidgets_highlight .il { color: #666666 } /* Literal.Number.Integer.Long */ 75 | -------------------------------------------------------------------------------- /wildewidgets/widgets/modals.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Any 4 | 5 | from django.core.exceptions import ImproperlyConfigured 6 | 7 | from .base import Block 8 | from .forms import CrispyFormWidget 9 | 10 | if TYPE_CHECKING: 11 | from django.forms import Form 12 | 13 | 14 | class ModalWidget(Block): 15 | """ 16 | Renders a Bootstrap 5 Modal dialog. 17 | 18 | This widget creates a Bootstrap modal dialog with a header, body, and footer. 19 | The modal can be triggered by a button or link with a data-target attribute 20 | matching the modal's ID. 21 | 22 | Example: 23 | .. code-block:: python 24 | 25 | from wildewidgets import ModalWidget, Block 26 | 27 | # Create a simple modal 28 | modal = ModalWidget( 29 | modal_id="example-modal", 30 | modal_title="Important Information", 31 | modal_body=Block("This is the modal content"), 32 | modal_size="lg" 33 | ) 34 | 35 | Attributes: 36 | template_name: Path to the template for rendering the modal 37 | modal_id: Unique identifier for the modal (used in triggering elements) 38 | modal_title: Text to display in the modal header 39 | modal_body: Content to display in the modal body (typically a Block) 40 | modal_size: Size of the modal dialog (None, 'sm', 'lg', or 'xl') 41 | 42 | Keyword Args: 43 | modal_id: The CSS ID of the modal 44 | modal_title: The title of the modal 45 | modal_body: The body of the modal, any Block 46 | modal_size: The size of the modal (None, 'sm', 'lg', or 'xl') 47 | **kwargs: Additional attributes passed to the parent Block class 48 | 49 | """ 50 | 51 | template_name: str = "wildewidgets/modal.html" 52 | modal_id: str | None = None 53 | modal_title: str | None = None 54 | modal_body: str | None = None 55 | modal_size: str | None = None 56 | 57 | def __init__( 58 | self, 59 | modal_id: str | None = None, 60 | modal_title: str | None = None, 61 | modal_body: str | None = None, 62 | modal_size: str | None = None, 63 | **kwargs: Any, 64 | ): 65 | self.modal_id = modal_id if modal_id else self.modal_id 66 | self.modal_title = modal_title if modal_title else self.modal_title 67 | self.modal_body = modal_body if modal_body else self.modal_body 68 | self.modal_size = modal_size if modal_size else self.modal_size 69 | super().__init__(**kwargs) 70 | 71 | def get_context_data(self, *args: Any, **kwargs: Any) -> dict[str, Any]: 72 | """ 73 | Prepare the context data for the modal template. 74 | 75 | This method adds the modal-specific attributes to the template context 76 | to be used in rendering the modal dialog. 77 | 78 | Args: 79 | *args: Variable length argument list 80 | **kwargs: Arbitrary keyword arguments 81 | 82 | Returns: 83 | dict: The updated context dictionary with modal attributes 84 | 85 | """ 86 | kwargs = super().get_context_data(*args, **kwargs) 87 | kwargs["modal_id"] = self.modal_id 88 | kwargs["modal_title"] = self.modal_title 89 | kwargs["modal_body"] = self.modal_body 90 | kwargs["modal_size"] = self.modal_size 91 | return kwargs 92 | 93 | 94 | class CrispyFormModalWidget(ModalWidget): 95 | """ 96 | A modal dialog containing a Django form rendered with django-crispy-forms. 97 | 98 | This specialized modal automatically places a form in the modal body, 99 | making it easy to create form dialogs for user input. It handles the 100 | rendering of the form using the CrispyFormWidget. 101 | 102 | Example: 103 | .. code-block:: python 104 | 105 | from django import forms 106 | from wildewidgets import CrispyFormModalWidget 107 | 108 | class ContactForm(forms.Form): 109 | name = forms.CharField() 110 | email = forms.EmailField() 111 | message = forms.CharField(widget=forms.Textarea) 112 | 113 | # Create a modal with the form 114 | modal = CrispyFormModalWidget( 115 | modal_id="contact-modal", 116 | modal_title="Contact Us", 117 | form_class=ContactForm 118 | ) 119 | 120 | Attributes: 121 | form_class: The form class to instantiate and render in the modal 122 | form: An already instantiated form to render in the modal 123 | 124 | Args: 125 | form: An instantiated form to render in the modal 126 | form_class: A form class to instantiate and render in the modal 127 | **kwargs: Additional arguments passed to the parent ModalWidget class 128 | (modal_id, modal_title, modal_size) 129 | 130 | Raises: 131 | ImproperlyConfigured: If neither form nor form_class is provided 132 | 133 | """ 134 | 135 | form_class: type[Form] | None = None 136 | form: Form | None = None 137 | 138 | def __init__( 139 | self, 140 | form: Form | None = None, 141 | form_class: type[Form] | None = None, 142 | **kwargs: Any, 143 | ): 144 | if not form: 145 | form = self.form 146 | if not form_class: 147 | form_class = self.form_class 148 | 149 | if form_class: 150 | modal_form = form_class() 151 | elif form: 152 | modal_form = form 153 | else: 154 | msg = "Either 'form_class' or 'form' must be set" 155 | raise ImproperlyConfigured(msg) 156 | modal_body = CrispyFormWidget(form=modal_form) 157 | kwargs["modal_body"] = modal_body 158 | super().__init__(**kwargs) 159 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/categorychart.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 157 | 158 | -------------------------------------------------------------------------------- /wildewidgets/widgets/charts/altair.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import random 4 | from typing import Any 5 | 6 | from django import template 7 | 8 | from wildewidgets.views import JSONDataView 9 | 10 | from ..base import Widget 11 | 12 | 13 | class AltairChart(Widget, JSONDataView): 14 | """ 15 | A widget for rendering Altair charts in Django applications. 16 | 17 | This class provides a wrapper around Altair charts, making them easy to integrate 18 | into Django templates. It supports both synchronous and asynchronous loading of 19 | chart data, and allows for customization of the chart's appearance. 20 | 21 | The chart content is rendered using a Django template, and the chart data is 22 | loaded via a JSON endpoint when in asynchronous mode. 23 | 24 | Example: 25 | .. code-block:: python 26 | 27 | import altair as alt 28 | from wildewidgets.widgets.charts.altair import AltairChart 29 | 30 | class MyBarChart(AltairChart): 31 | def load(self): 32 | # Create an Altair chart 33 | data = pd.DataFrame({ 34 | 'category': ['A', 'B', 'C'], 35 | 'value': [10, 20, 30] 36 | }) 37 | chart = alt.Chart(data).mark_bar().encode( 38 | x='category', 39 | y='value' 40 | ) 41 | self.set_data(chart) 42 | 43 | """ 44 | 45 | #: The Django template file to render the chart 46 | template_file: str = "wildewidgets/altairchart.html" 47 | #: The title of the chart, can be set in the options 48 | title: str | None = None 49 | #: Default width for the chart, can be overridden in options 50 | width: str = "100%" 51 | #: Default height for the chart, can be overridden in options 52 | height: str = "300px" 53 | 54 | def __init__(self, *args, **kwargs) -> None: # noqa: ARG002 55 | """ 56 | Initialize the Altair chart widget. 57 | 58 | Args: 59 | *args: Variable length argument list (not used) 60 | **kwargs: Arbitrary keyword arguments 61 | width: Override the default chart width 62 | height: Override the default chart height 63 | title: Set the chart title 64 | 65 | Note: 66 | The chart data is not loaded during initialization. It will be 67 | loaded when get_context_data is called. 68 | 69 | """ 70 | self.data = None 71 | self.chart_options = { 72 | "width": kwargs.get("width", self.width), 73 | "height": kwargs.get("height", self.height), 74 | "title": kwargs.get("title", self.title), 75 | } 76 | 77 | def get_content(self, **kwargs) -> str: # noqa: ARG002 78 | """ 79 | Render the chart as HTML content. 80 | 81 | This method renders the chart using the specified template file. If the chart 82 | data has not been loaded yet, it will set the ``async`` flag to 83 | ``True``, which tells the template to load the data asynchronously via a 84 | JSON endpoint. 85 | 86 | Args: 87 | **kwargs: Arbitrary keyword arguments (not used) 88 | 89 | Returns: 90 | str: The rendered HTML content for the chart 91 | 92 | """ 93 | chart_id = random.randrange(0, 1000) # noqa: S311 94 | template_file = self.template_file 95 | context: dict[str, Any] = ( 96 | self.get_context_data() if self.data else {"async": True} 97 | ) 98 | html_template = template.loader.get_template(template_file) 99 | context["options"] = self.chart_options 100 | context["name"] = f"altair_chart_{chart_id}" 101 | context["wildewidgetclass"] = self.__class__.__name__ 102 | return html_template.render(context) 103 | 104 | def __str__(self) -> str: 105 | """ 106 | Return the string representation of the chart. 107 | 108 | This method allows the chart to be used directly in Django templates. 109 | 110 | Returns: 111 | str: The rendered HTML content for the chart 112 | 113 | """ 114 | return self.get_content() 115 | 116 | def get_context_data(self, **kwargs) -> dict[str, Any]: 117 | """ 118 | Get the context data for rendering the chart. 119 | 120 | This method loads the chart data if it hasn't been loaded yet and 121 | adds it to the context data. 122 | 123 | Args: 124 | **kwargs: Arbitrary keyword arguments passed to the parent method 125 | 126 | Returns: 127 | dict: The context data for rendering the chart 128 | 129 | """ 130 | context = super().get_context_data(**kwargs) 131 | self.load() 132 | context.update({"data": self.data}) 133 | return context 134 | 135 | def set_data(self, spec, set_size: bool = True): 136 | """ 137 | Set the chart data from an Altair chart specification. 138 | 139 | This method converts an Altair chart specification into a dictionary 140 | representation that can be serialized to JSON. It also optionally sets 141 | the chart to use container-based sizing. 142 | 143 | Args: 144 | spec: The Altair chart specification 145 | set_size: Whether to set the chart to use container-based sizing 146 | When True, the chart will fill its container element. 147 | When False, the chart will use its own width and height settings. 148 | 149 | """ 150 | if set_size: 151 | self.data = spec.properties(width="container", height="container").to_dict() 152 | else: 153 | self.data = spec.to_dict() 154 | 155 | def load(self) -> None: 156 | """ 157 | Load the chart data. 158 | 159 | This method should be overridden by subclasses to load the chart data. 160 | The implementation should create an Altair chart and call set_data() with it. 161 | 162 | The default implementation does nothing. 163 | 164 | Example: 165 | .. code-block:: python 166 | 167 | def load(self): 168 | # Load data from a source 169 | data = pd.DataFrame(...) 170 | 171 | # Create an Altair chart 172 | chart = alt.Chart(data).mark_bar().encode(...) 173 | 174 | # Set the chart data 175 | self.set_data(chart) 176 | 177 | """ 178 | -------------------------------------------------------------------------------- /wildewidgets/views/tables.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Any 4 | 5 | from django.core.exceptions import ImproperlyConfigured 6 | from django.http import Http404, HttpResponseRedirect 7 | from django.views.generic import View 8 | from django.views.generic.base import TemplateView 9 | 10 | if TYPE_CHECKING: 11 | from django.http import HttpRequest 12 | 13 | from ..widgets import BaseDataTable 14 | 15 | 16 | class TableActionFormView(View): 17 | """ 18 | A view that processes form actions submitted from a data table. 19 | 20 | This view handles POST requests containing table actions (like bulk delete, 21 | approve, etc.) and delegates the processing to action-specific methods. It 22 | follows a convention-based approach where actions are processed by methods 23 | named `process_{action}_action`. 24 | 25 | To use this view: 26 | 27 | 1. Subclass it and implement methods for each action (e.g., `process_delete_action`) 28 | 2. Set the `url` attribute to specify where to redirect after processing 29 | 3. Map the view to a URL in your URLconf 30 | 31 | Example: 32 | .. code-block:: python 33 | 34 | from django.http import HttpResponseRedirect 35 | from django.urls import reverse_lazy 36 | from django.views.generic import View 37 | from wildewidgets import TableActionFormView 38 | 39 | from myapp.models import User 40 | 41 | class UserBulkActionView(TableActionFormView): 42 | url = reverse_lazy('user-list') 43 | 44 | def process_delete_action(self, items): 45 | User.objects.filter(id__in=items).delete() 46 | 47 | def process_activate_action(self, items): 48 | User.objects.filter(id__in=items).update(is_active=True) 49 | 50 | """ 51 | 52 | #: The HTTP methods this view will respond to 53 | http_method_names: list[str] = ["post"] # noqa: RUF012 54 | #: The URL to redirect to after processing the form action 55 | url: str | None = None 56 | 57 | def process_form_action(self, action: str, items: list[str]) -> None: 58 | """ 59 | Process a form action by delegating to an action-specific method. 60 | 61 | This method looks for a method named `process_{action}_action` and calls 62 | it with the list of selected items. If no such method exists, the action 63 | is silently ignored. 64 | 65 | Args: 66 | action: The name of the action to process (e.g., "delete", "approve") 67 | items: List of item IDs that the action should be applied to 68 | 69 | Example: 70 | If `action` is "delete", this method will try to call 71 | `self.process_delete_action(items)` 72 | 73 | """ 74 | method_name = f"process_{action}_action" 75 | if hasattr(self, method_name): 76 | getattr(self, method_name)(items) 77 | 78 | def post( 79 | self, 80 | request: HttpRequest, 81 | *args: Any, # noqa: ARG002 82 | **kwargs: Any, # noqa: ARG002 83 | ) -> HttpResponseRedirect | Http404: 84 | """ 85 | Handle POST requests by processing the form action and redirecting. 86 | 87 | This method: 88 | 89 | 1. Extracts the selected items and action from the POST data 90 | 2. Delegates to `process_form_action` for the actual processing 91 | 3. Redirects to the URL specified by the `url` attribute 92 | 93 | Args: 94 | request: The HTTP request object 95 | *args: Additional positional arguments (not used) 96 | **kwargs: Additional keyword arguments (not used) 97 | 98 | Returns: 99 | HttpResponseRedirect: Redirect to the specified URL after successful 100 | processing 101 | Http404: If the request doesn't include required POST data 102 | 103 | Raises: 104 | ImproperlyConfigured: If the `url` attribute is not set 105 | 106 | """ 107 | checkboxes = request.POST.getlist("checkbox") 108 | action = request.POST.get("action") 109 | if not action or not checkboxes: 110 | return Http404("POST request did not include the necessary fields") 111 | self.process_form_action(action, checkboxes) 112 | if not self.url: 113 | msg = f"You must set a url attribute on {self.__class__.__name__}" 114 | raise ImproperlyConfigured(msg) 115 | return HttpResponseRedirect(self.url) 116 | 117 | 118 | class TableView(TemplateView): 119 | """ 120 | A template view that renders a data table. 121 | 122 | This view simplifies the common pattern of rendering a template that includes 123 | a data table. It instantiates the specified table class and adds it to the 124 | template context. 125 | 126 | To use this view: 127 | 1. Subclass it and set the `table_class` attribute to your table class 128 | 2. Set `template_name` to the template that will render the table 129 | 130 | Attributes: 131 | table_class: The table widget class to instantiate and render 132 | 133 | Example: 134 | .. code-block:: python 135 | 136 | from wildewidgets import TableView, BaseModelTable 137 | from myapp.models import User 138 | 139 | class UserTable(BaseModelTable): 140 | model = User 141 | fields: List[str] = ["username", "email", "date_joined"] 142 | 143 | class UserTableView(TableView): 144 | table_class = UserTable 145 | 146 | """ 147 | 148 | #: The table class to use for rendering the table. 149 | table_class: type[BaseDataTable] | None = None 150 | 151 | def get_context_data(self, **kwargs: Any) -> dict[str, Any]: 152 | """ 153 | Add the instantiated table to the template context. 154 | 155 | This method instantiates the table specified by `table_class` and adds 156 | it to the template context with the key 'table'. 157 | 158 | Args: 159 | **kwargs: Additional context variables 160 | 161 | Returns: 162 | dict: The updated template context with the table instance 163 | 164 | Raises: 165 | ImproperlyConfigured: If `table_class` is not set 166 | 167 | """ 168 | if not self.table_class: 169 | msg = f"You must set a table_class attribute on {self.__class__.__name__}" 170 | raise ImproperlyConfigured(msg) 171 | kwargs["table"] = self.table_class() # pylint: disable=not-callable 172 | return super().get_context_data(**kwargs) 173 | -------------------------------------------------------------------------------- /docs/business_charts.rst: -------------------------------------------------------------------------------- 1 | *************** 2 | Business Charts 3 | *************** 4 | 5 | Business charts use a subset of ChartJS to build basic charts. Types include Bar, Horizontal Bar, Stacked Bar, Pie, Doughnut, and Histogram. 6 | 7 | Usage 8 | ===== 9 | 10 | Without AJAX 11 | ------------ 12 | 13 | With a chart that doesn't use ajax, the data will load before the page has been loaded. Large datasets may cause the page to load too slowly, so this is best for smaller datasets. 14 | 15 | In your view code, import the appropriate chart:: 16 | 17 | from wildewidgets import ( 18 | BarChart, 19 | DoughnutChart, 20 | HorizontalStackedBarChart, 21 | HorizontalBarChart, 22 | PieChart, 23 | StackedBarChart, 24 | ) 25 | 26 | and define the chart in your view:: 27 | 28 | class HomeView(TemplateView): 29 | template_name = "core/home.html" 30 | 31 | def get_context_data(self, **kwargs): 32 | barchart = HorizontalStackedBarChart(title="New Customers Through July", money=True, legend=True, width='500', color=False) 33 | barchart.set_categories(["January", "February", "March", "April", "May", "June", "July"]) 34 | barchart.add_dataset([75, 44, 92, 11, 44, 95, 35], "Central") 35 | barchart.add_dataset([41, 92, 18, 35, 73, 87, 92], "Eastside") 36 | barchart.add_dataset([87, 21, 94, 13, 90, 13, 65], "Westside") 37 | kwargs['barchart'] = barchart 38 | return super().get_context_data(**kwargs) 39 | 40 | In your template, simply display the chart:: 41 | 42 | {{barchart}} 43 | 44 | With Ajax 45 | --------- 46 | 47 | With a chart that does use ajax, the data will load after the page has been loaded. This is the best choice for performance with large datasets, especially if you have multiple charts loading on a page. With ajax, the charts load in the background. 48 | 49 | Create a file called ``wildewidgets.py`` in your app directory and create a new class derived from the chart class that you want. You'll need to either override ``get_categories``, ``get_dataset_labels`` and ``get_datasets``, or override ``load``, where you can just call the functions you need to call to set up your chart:: 50 | 51 | from wildewidgets import StackedBarChart 52 | 53 | class TestChart(StackedBarChart): 54 | 55 | def get_categories(self): 56 | """Return 7 labels for the x-axis.""" 57 | return ["January", "February", "March", "April", "May", "June", "July"] 58 | 59 | def get_dataset_labels(self): 60 | """Return names of datasets.""" 61 | return ["Central", "Eastside", "Westside", "Central2", "Eastside2", "Westside2", "Central3", "Eastside3", "Westside3"] 62 | 63 | def get_datasets(self): 64 | """Return 3 datasets to plot.""" 65 | 66 | return [[750, 440, 920, 1100, 440, 950, 350], 67 | [410, 1920, 180, 300, 730, 870, 920], 68 | [870, 210, 940, 3000, 900, 130, 650], 69 | [750, 440, 920, 1100, 440, 950, 350], 70 | [410, 920, 180, 2000, 730, 870, 920], 71 | [870, 210, 940, 300, 900, 130, 650], 72 | [750, 440, 920, 1100, 440, 950, 3500], 73 | [410, 920, 180, 3000, 730, 870, 920], 74 | [870, 210, 940, 300, 900, 130, 650]] 75 | 76 | Then in your view code, use this class instead:: 77 | 78 | from .wildewidgets import TestChart 79 | 80 | class HomeView(TemplateView): 81 | template_name = "core/home.html" 82 | 83 | def get_context_data(self, **kwargs): 84 | kwargs['barchart'] = TestChart(width='500', height='400', thousands=True) 85 | return super().get_context_data(**kwargs) 86 | 87 | In your template, display the chart as before:: 88 | 89 | {{barchart}} 90 | 91 | Histograms 92 | ---------- 93 | 94 | Histograms are built slightly differently. You'll need to call the object's ``build`` function, with arguments for a list of values, and the number of bins you want. The histogram will utilize ajax if the build function is called in the ``load`` function. 95 | 96 | Without AJAX 97 | ^^^^^^^^^^^^ 98 | 99 | :: 100 | 101 | class TestHistogram(Histogram): # without ajax 102 | 103 | def __init__(self, *args, **kwargs): 104 | super().__init__(*args, **kwargs) 105 | mu = 0 106 | sigma = 50 107 | nums = [] 108 | bin_count = 40 109 | for i in range(10000): 110 | temp = random.gauss(mu,sigma) 111 | nums.append(temp) 112 | 113 | self.build(nums, bin_count) 114 | 115 | With AJAX 116 | ^^^^^^^^^ 117 | 118 | :: 119 | 120 | class TestHorizontalHistogram(HorizontalHistogram): # with ajax 121 | 122 | def __init__(self, *args, **kwargs): 123 | super().__init__(*args, **kwargs) 124 | self.set_color(False) 125 | 126 | def load(self): 127 | mu = 100 128 | sigma = 30 129 | nums = [] 130 | bin_count = 50 131 | for i in range(10000): 132 | temp = random.gauss(mu,sigma) 133 | nums.append(temp) 134 | 135 | self.build(nums, bin_count) 136 | 137 | Options 138 | ======= 139 | 140 | There are a number of available Charts: 141 | 142 | * BarChart 143 | * HorizontalBarChart 144 | * StackedBarChart 145 | * HorizontalStackedBarChart 146 | * PieChart 147 | * DoughnutChart 148 | * Histogram 149 | * HorizontalHistogram 150 | 151 | There are a number of options you can set for a specific chart:: 152 | 153 | width: chart width in pixels (default: 400) 154 | height: chart height in pixels (default: 400) 155 | title: title text (default: None) 156 | color: use color as opposed to grayscale (default: True) 157 | legend: whether or not to show the legend - True/False (default: False) 158 | legend-position: top, right, bottom, left (default: left) 159 | thousands: if set to true, numbers are abbreviated as in 1K 5M, ... (default: False) 160 | money: whether or not the value is money (default: False) 161 | url: a click on a segment of a chart will redirect to this URL, with parameters label and value 162 | 163 | Colors 164 | ------ 165 | 166 | You can also customize the colors by either overriding the class variable ``COLORS`` or calling the member function ``set_colors``. The format is a list of RGB tuples. 167 | 168 | Fonts 169 | ----- 170 | 171 | To customize the fonts globally, the available Django settings are:: 172 | 173 | CHARTJS_FONT_FAMILY = "'Vaud', sans-serif" 174 | CHARTJS_TITLE_FONT_SIZE = '18' 175 | CHARTJS_TITLE_FONT_STYLE = 'normal' 176 | CHARTJS_TITLE_PADDING = '0' 177 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv pip compile --group demo --group docs --group test pyproject.toml -o requirements.txt 3 | alabaster==0.7.16 4 | # via sphinx 5 | altair==4.2.0 6 | # via django-wildewidgets (pyproject.toml:demo) 7 | appnope==0.1.4 8 | # via ipython 9 | asgiref==3.9.1 10 | # via django 11 | asttokens==3.0.0 12 | # via stack-data 13 | attrs==25.3.0 14 | # via 15 | # jsonschema 16 | # referencing 17 | babel==2.17.0 18 | # via sphinx 19 | backcall==0.2.0 20 | # via ipython 21 | beautifulsoup4==4.13.4 22 | # via pydata-sphinx-theme 23 | bleach==5.0.1 24 | # via 25 | # django-wildewidgets (pyproject.toml:demo) 26 | # django-markdownify 27 | certifi==2025.7.14 28 | # via requests 29 | charset-normalizer==3.4.2 30 | # via requests 31 | coverage==7.9.2 32 | # via 33 | # django-wildewidgets (pyproject.toml:test) 34 | # django-coverage-plugin 35 | crispy-bootstrap5==2025.6 36 | # via 37 | # django-wildewidgets (pyproject.toml) 38 | # django-wildewidgets 39 | crython==0.2.0 40 | # via django-wildewidgets (pyproject.toml:demo) 41 | decorator==5.2.1 42 | # via ipython 43 | django==5.2.4 44 | # via 45 | # django-wildewidgets (pyproject.toml) 46 | # crispy-bootstrap5 47 | # django-braces 48 | # django-crequest 49 | # django-crispy-forms 50 | # django-debug-toolbar 51 | # django-extensions 52 | # django-generic-json-views 53 | # django-js-reverse 54 | # django-markdownify 55 | # django-queryinspect 56 | # django-redis 57 | # django-storages 58 | # django-wildewidgets 59 | # django-xff 60 | # sphinxcontrib-django 61 | django-autocomplete-light==3.5.1 62 | # via django-wildewidgets (pyproject.toml:demo) 63 | django-book-manager==0.3.2 64 | # via django-wildewidgets (pyproject.toml:demo) 65 | django-braces==1.14.0 66 | # via django-wildewidgets (pyproject.toml:demo) 67 | django-chartjs==2.2.1 68 | # via django-wildewidgets (pyproject.toml:demo) 69 | django-coverage-plugin==3.1.1 70 | # via django-wildewidgets (pyproject.toml:test) 71 | django-crequest==2018.5.11 72 | # via django-wildewidgets (pyproject.toml:demo) 73 | django-crispy-forms==2.4 74 | # via 75 | # django-wildewidgets (pyproject.toml) 76 | # crispy-bootstrap5 77 | # django-wildewidgets 78 | django-debug-toolbar==2.2 79 | # via django-wildewidgets (pyproject.toml:demo) 80 | django-environ==0.4.5 81 | # via django-wildewidgets (pyproject.toml:demo) 82 | django-extensions==4.1 83 | # via 84 | # django-wildewidgets (pyproject.toml:demo) 85 | # django-book-manager 86 | django-generic-json-views==0.8 87 | # via django-wildewidgets (pyproject.toml:demo) 88 | django-js-reverse==0.9.1 89 | # via django-wildewidgets (pyproject.toml:demo) 90 | django-markdownify==0.9.2 91 | # via django-wildewidgets (pyproject.toml:demo) 92 | django-queryinspect==1.1.0 93 | # via django-wildewidgets (pyproject.toml:demo) 94 | django-redis==4.11.0 95 | # via django-wildewidgets (pyproject.toml:demo) 96 | django-sass-processor==1.2.2 97 | # via django-wildewidgets (pyproject.toml:demo) 98 | django-storages==1.9.1 99 | # via django-wildewidgets (pyproject.toml:demo) 100 | django-theme-academy==0.3.2 101 | # via django-wildewidgets (pyproject.toml:demo) 102 | django-wildewidgets==1.1.7 103 | # via django-theme-academy 104 | django-xff==1.3.0 105 | # via django-wildewidgets (pyproject.toml:demo) 106 | docutils==0.19 107 | # via 108 | # pydata-sphinx-theme 109 | # sphinx 110 | entrypoints==0.4 111 | # via altair 112 | executing==2.2.0 113 | # via stack-data 114 | factory-boy==3.3.3 115 | # via django-wildewidgets (pyproject.toml:test) 116 | faker==37.4.2 117 | # via factory-boy 118 | gunicorn==23.0.0 119 | # via django-wildewidgets (pyproject.toml:demo) 120 | idna==3.10 121 | # via requests 122 | imagesize==1.4.1 123 | # via sphinx 124 | ipython==8.6.0 125 | # via django-wildewidgets (pyproject.toml:demo) 126 | jedi==0.19.2 127 | # via ipython 128 | jinja2==3.0.0 129 | # via 130 | # altair 131 | # sphinx 132 | jsonschema==4.25.0 133 | # via altair 134 | jsonschema-specifications==2025.4.1 135 | # via jsonschema 136 | libsass==0.22.0 137 | # via django-wildewidgets (pyproject.toml:demo) 138 | lxml==6.0.0 139 | # via unittest-xml-reporting 140 | markdown==3.8.2 141 | # via django-markdownify 142 | markupsafe==3.0.2 143 | # via jinja2 144 | matplotlib-inline==0.1.7 145 | # via ipython 146 | mysqlclient==2.1.1 147 | # via django-wildewidgets (pyproject.toml:demo) 148 | nameparser==1.1.3 149 | # via django-book-manager 150 | numpy==2.3.1 151 | # via 152 | # altair 153 | # pandas 154 | packaging==25.0 155 | # via 156 | # gunicorn 157 | # pydata-sphinx-theme 158 | # sphinx 159 | pandas==2.3.1 160 | # via altair 161 | parso==0.8.4 162 | # via jedi 163 | pexpect==4.9.0 164 | # via ipython 165 | pickleshare==0.7.5 166 | # via ipython 167 | pprintpp==0.4.0 168 | # via sphinxcontrib-django 169 | prompt-toolkit==3.0.51 170 | # via ipython 171 | ptyprocess==0.7.0 172 | # via pexpect 173 | pure-eval==0.2.3 174 | # via stack-data 175 | pydata-sphinx-theme==0.9.0 176 | # via django-wildewidgets (pyproject.toml:docs) 177 | pygments==2.19.2 178 | # via 179 | # ipython 180 | # sphinx 181 | python-dateutil==2.9.0.post0 182 | # via pandas 183 | pytz==2022.6 184 | # via 185 | # django-wildewidgets (pyproject.toml:demo) 186 | # pandas 187 | redis==6.2.0 188 | # via django-redis 189 | referencing==0.36.2 190 | # via 191 | # jsonschema 192 | # jsonschema-specifications 193 | requests==2.32.4 194 | # via 195 | # sphinx 196 | # sphinxcontrib-images 197 | rpds-py==0.26.0 198 | # via 199 | # jsonschema 200 | # referencing 201 | setuptools==80.9.0 202 | # via django-wildewidgets (pyproject.toml:docs) 203 | six==1.10.0 204 | # via 205 | # bleach 206 | # django-autocomplete-light 207 | # django-braces 208 | # django-generic-json-views 209 | # python-dateutil 210 | snowballstemmer==3.0.1 211 | # via sphinx 212 | soupsieve==2.7 213 | # via beautifulsoup4 214 | sphinx==5.2.3 215 | # via 216 | # django-wildewidgets (pyproject.toml:docs) 217 | # pydata-sphinx-theme 218 | # sphinxcontrib-django 219 | # sphinxcontrib-images 220 | sphinxcontrib-applehelp==2.0.0 221 | # via sphinx 222 | sphinxcontrib-devhelp==2.0.0 223 | # via sphinx 224 | sphinxcontrib-django==2.5 225 | # via django-wildewidgets (pyproject.toml:docs) 226 | sphinxcontrib-htmlhelp==2.1.0 227 | # via sphinx 228 | sphinxcontrib-images==0.9.4 229 | # via django-wildewidgets (pyproject.toml:docs) 230 | sphinxcontrib-jsmath==1.0.1 231 | # via sphinx 232 | sphinxcontrib-qthelp==2.0.0 233 | # via sphinx 234 | sphinxcontrib-serializinghtml==2.0.0 235 | # via sphinx 236 | sqlparse==0.5.3 237 | # via 238 | # django 239 | # django-debug-toolbar 240 | stack-data==0.6.3 241 | # via ipython 242 | structlog==22.2.0 243 | # via django-wildewidgets (pyproject.toml:demo) 244 | testfixtures==9.1.0 245 | # via django-wildewidgets (pyproject.toml:test) 246 | tinycss2==1.1.1 247 | # via bleach 248 | toolz==1.0.0 249 | # via altair 250 | traitlets==5.14.3 251 | # via 252 | # ipython 253 | # matplotlib-inline 254 | typing-extensions==4.14.1 255 | # via 256 | # beautifulsoup4 257 | # referencing 258 | tzdata==2025.2 259 | # via 260 | # faker 261 | # pandas 262 | unittest-xml-reporting==3.2.0 263 | # via django-wildewidgets (pyproject.toml:test) 264 | urllib3==2.5.0 265 | # via requests 266 | wcwidth==0.2.13 267 | # via prompt-toolkit 268 | webencodings==0.5.1 269 | # via 270 | # bleach 271 | # tinycss2 272 | -------------------------------------------------------------------------------- /wildewidgets/widgets/tables/components.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | 5 | 6 | class DataTableColumn: 7 | """ 8 | Defines a column configuration for a :py:class:`wildewidgets.DataTable`. 9 | 10 | This class stores the configuration for a single column in a ``DataTable``, 11 | including display options, behavior, and formatting settings. 12 | 13 | Example: 14 | .. code-block:: python 15 | 16 | from wildewidgets import DataTableColumn 17 | 18 | # Create a right-aligned numeric column that can be sorted 19 | column = DataTableColumn( 20 | field="amount", 21 | verbose_name="Amount ($)", 22 | searchable=True, 23 | sortable=True, 24 | align="right" 25 | ) 26 | 27 | Args: 28 | field: Field name or identifier for the column 29 | 30 | Keyword Args: 31 | verbose_name: Human-readable name for the column header 32 | (defaults to capitalized field name) 33 | searchable: Whether this column is included in global searches 34 | sortable: Whether the table can be sorted by this column 35 | align: Horizontal alignment of cell content ("left", "right", "center") 36 | head_align: Horizontal alignment of the column header 37 | ("left", "right", "center") 38 | visible: Whether the column is visible in the table 39 | wrap: Whether to wrap text content in the column cells 40 | 41 | """ 42 | 43 | def __init__( 44 | self, 45 | field: str, 46 | verbose_name: str | None = None, 47 | searchable: bool = False, 48 | sortable: bool = False, 49 | align: str = "left", 50 | head_align: str = "left", 51 | visible: bool = True, 52 | wrap: bool = True, 53 | ): 54 | self.field = field 55 | self.verbose_name = ( 56 | verbose_name if verbose_name else self.field.capitalize() 57 | ) 58 | self.searchable = searchable 59 | self.sortable = sortable 60 | self.align = align 61 | self.head_align = head_align 62 | self.visible = visible 63 | self.wrap = wrap 64 | 65 | 66 | class DataTableFilter: 67 | """ 68 | Defines a filter control for a :py:class:`wildewidgets.DataTable` column. 69 | 70 | This class represents a UI control for filtering data in a specific column, 71 | typically displayed as a dropdown list of options.If no default is 72 | provided, the filter is off by default. 73 | 74 | Example: 75 | .. code-block:: python 76 | 77 | from wildewidgets import DataTableFilter 78 | 79 | # Create a status filter with custom options 80 | filter = DataTableFilter( 81 | header="Filter by Status", 82 | ) 83 | filter.add_choice("Active", "active") 84 | filter.add_choice("Inactive", "inactive") 85 | filter.add_choice("Pending", "pending") 86 | 87 | # Add the filter to the table 88 | table.add_filter("status", filter) 89 | 90 | Keyword Args: 91 | header: Optional header content for the filter 92 | 93 | """ 94 | 95 | def __init__(self, header: Any | None = None) -> None: 96 | self.header = header 97 | self.choices: list[tuple[str, str]] = [("Any", "")] 98 | self.default: bool = False 99 | self.default_value: str | None = None 100 | self.default_label: str | None = None 101 | 102 | def add_choice( 103 | self, label: str, value: str, default: bool = False 104 | ) -> None: 105 | """ 106 | Add a filter option to the choices list. 107 | 108 | Args: 109 | label: The human-readable label displayed in the UI 110 | value: The value used for filtering when this option is selected 111 | default: Whether this option is the default selected option 112 | 113 | """ 114 | self.choices.append((label, value)) 115 | if default: 116 | self.default_value = value 117 | self.default_label = label 118 | self.default = True 119 | 120 | 121 | class DataTableStyler: 122 | """ 123 | Defines conditional styling rules for DataTable cells or rows. 124 | 125 | This class allows you to apply CSS classes to table cells or rows 126 | based on the content of a specific cell. It's used for conditional 127 | formatting, like highlighting negative values in red or flagging 128 | certain status values. 129 | 130 | Example: 131 | .. code-block:: python 132 | 133 | from wildewidgets import BaseDataTable, DataTableStyler, DataTableColumn 134 | 135 | # Style the status column with "text-danger" when value is "error" 136 | styler = DataTableStyler( 137 | is_row=False, 138 | test_cell="status", 139 | cell_value="error", 140 | css_class="text-danger" 141 | ) 142 | 143 | # Style the entire row with "table-warning" when status is "pending" 144 | row_styler = DataTableStyler( 145 | is_row=True, 146 | test_cell="status", 147 | cell_value="pending", 148 | css_class="table-warning" 149 | ) 150 | 151 | table = BaseDataTable( 152 | title="My Data Table", 153 | columns=[ 154 | DataTableColumn(field="name", verbose_name="Name"), 155 | DataTableColumn(field="status", verbose_name="Status"), 156 | ], 157 | ) 158 | 159 | table.add_styler(styler) 160 | table.add_styler(row_styler) 161 | 162 | Args: 163 | is_row: Whether to apply styling to the entire row (True) 164 | or just a cell (False) 165 | test_cell: The name of the column to test for the condition 166 | cell_value: The value to compare against for the condition 167 | css_class: The CSS class to apply when the condition is met 168 | 169 | Keyword Args: 170 | target_cell: The name of the column to style 171 | (if None, uses :py:attr:`test_cell`) 172 | 173 | 174 | """ 175 | 176 | def __init__( 177 | self, 178 | is_row: bool, 179 | test_cell: str, 180 | cell_value: Any, 181 | css_class: str, 182 | target_cell: str | None = None, 183 | ): 184 | self.is_row = is_row 185 | self.test_cell = test_cell 186 | self.cell_value = cell_value 187 | self.css_class = css_class 188 | self.target_cell = target_cell 189 | self.test_index = 0 190 | self.target_index = 0 191 | 192 | 193 | class DataTableForm: 194 | """ 195 | Provides form handling for bulk actions in a DataTable. 196 | 197 | This class integrates form functionality into a DataTable, allowing 198 | users to select multiple rows using checkboxes and perform actions 199 | on the selected rows, such as delete, approve, or export. 200 | 201 | Note: 202 | This class is created automatically by the 203 | :py:class:`wildewidgets.DataTable` or its subclasses when 204 | :py:attr:`wildewidgets.DataTable.form_actions` are defined. 205 | 206 | You typically do not need to instantiate this class directly. 207 | 208 | The form is only visible if the table has 209 | :py:attr:`wildewidgets.DataTable.form_actions` defined. Form actions and 210 | URL are retrieved from the table instance. 211 | 212 | Args: 213 | table: The DataTable instance this form belongs to 214 | 215 | """ 216 | 217 | def __init__(self, table: Any): 218 | if table.has_form_actions(): 219 | self.is_visible: bool = True 220 | else: 221 | self.is_visible = False 222 | self.actions = table.get_form_actions() 223 | self.url = table.form_url 224 | -------------------------------------------------------------------------------- /demo/demo/core/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponseRedirect 2 | from django.templatetags.static import static 3 | from django.urls import reverse, reverse_lazy 4 | from django.views.generic import TemplateView 5 | from wildewidgets import ( 6 | BreadcrumbBlock, 7 | MenuMixin, 8 | StandardWidgetMixin, 9 | VerticalDarkMenu, 10 | WidgetListLayout, 11 | ) 12 | 13 | from .wildewidgets import ( 14 | ApexChart1Card, 15 | ApexSparklineCard, 16 | AuthorListCard, 17 | AuthorListModelCardWidgetCard, 18 | BarChartCard, 19 | BookModelTableCard, 20 | CardCard, 21 | CodeCard, 22 | CollapseCard, 23 | CrispyFormCard, 24 | CrispyFormModalCard, 25 | DataTableCard, 26 | DonutCard, 27 | HistogramCard, 28 | HomeBlock, 29 | HorizontalBarChartCard, 30 | HorizontalHistogramCard, 31 | HorizontalLayoutCard, 32 | HTMLCard, 33 | MarkdownCard, 34 | ModalCard, 35 | PagedBookCard, 36 | PieCard, 37 | SciChartCard, 38 | SciSyncChartCard, 39 | StringCard, 40 | TabCard, 41 | TestTableCard, 42 | WidgetCellTableCard, 43 | ) 44 | 45 | 46 | class DemoMenu(VerticalDarkMenu): 47 | brand_image: str = static("core/images/dark_logo.png") 48 | brand_image_width: str = "100%" 49 | brand_text: str = "Wildewidgets Demo" 50 | brand_url: str = reverse_lazy("core:home") 51 | items = [ # noqa: RUF012 52 | ("Home", "core:home"), 53 | ("Tables", "core:tables"), 54 | ("Text Widgets", "core:text"), 55 | ("List Widgets", "core:list"), 56 | ("Structure Widgets", "core:structure"), 57 | ("Modal Widgets", "core:modal"), 58 | ( 59 | "Charts", 60 | [ 61 | ("Business Charts (ChartJS)", "core:charts"), 62 | ("Scientific Charts (Altair)", "core:altair"), 63 | ("Apex Charts", "core:apex"), 64 | ], 65 | ), 66 | ] 67 | 68 | 69 | class DemoBaseBreadcrumbs(BreadcrumbBlock): 70 | title_class = "fw-bold" 71 | 72 | def __init__(self, *args, **kwargs): 73 | super().__init__(*args, **kwargs) 74 | self.add_breadcrumb("Django Wildewidgets Demo", reverse_lazy("core:home")) 75 | 76 | 77 | class DemoStandardMixin(StandardWidgetMixin, MenuMixin): 78 | template_name = "core/intermediate.html" 79 | menu_class = DemoMenu 80 | 81 | 82 | class HomeView(DemoStandardMixin, TemplateView): 83 | menu_item = "Home" 84 | 85 | def get_content(self): 86 | return HomeBlock() 87 | 88 | def get_breadcrumbs(self): 89 | breadcrumbs = DemoBaseBreadcrumbs() 90 | breadcrumbs.add_breadcrumb("Home") 91 | return breadcrumbs 92 | 93 | 94 | class ChartView(DemoStandardMixin, TemplateView): 95 | menu_item = "Charts" 96 | 97 | def get_content(self): 98 | layout = WidgetListLayout("Basic Business Charts w/ ChartJS", css_class="mt-4") 99 | layout.add_widget(PieCard(), "Pie Chart", "pie-chart") 100 | layout.add_widget(DonutCard(), "AJAX Donut Chart", "circle") 101 | layout.add_widget(BarChartCard(), "Bar Chart", "bar-chart") 102 | layout.add_widget( 103 | HorizontalBarChartCard(), "AJAX Horizontal Bar Chart", "bar-chart-steps" 104 | ) 105 | layout.add_widget(HistogramCard(), "Histogram", "bar-chart") 106 | layout.add_widget( 107 | HorizontalHistogramCard(), "AJAX Horizontal Histogram", "bar-chart-steps" 108 | ) 109 | return layout 110 | 111 | def get_breadcrumbs(self): 112 | breadcrumbs = DemoBaseBreadcrumbs() 113 | breadcrumbs.add_breadcrumb("Business Charts") 114 | return breadcrumbs 115 | 116 | 117 | class AltairView(DemoStandardMixin, TemplateView): 118 | menu_item = "Charts" 119 | 120 | def get_content(self): 121 | layout = WidgetListLayout("Scientific Charts w/ Altair", css_class="mt-4") 122 | layout.add_widget(SciSyncChartCard(), "Altair Chart", "graph-up") 123 | layout.add_widget(SciChartCard(), "AJAX Altair Chart", "graph-down") 124 | return layout 125 | 126 | def get_breadcrumbs(self): 127 | breadcrumbs = DemoBaseBreadcrumbs() 128 | breadcrumbs.add_breadcrumb("Scientific Charts") 129 | return breadcrumbs 130 | 131 | 132 | class ApexChartView(DemoStandardMixin, TemplateView): 133 | menu_item = "Charts" 134 | 135 | def get_content(self): 136 | layout = WidgetListLayout("Apex Charts", css_class="mt-4") 137 | layout.add_widget(ApexChart1Card(), "AJAX Apex Chart", "graph-up") 138 | layout.add_widget(ApexSparklineCard(), "Apex Sparkline Chart", "graph-down") 139 | return layout 140 | 141 | def get_breadcrumbs(self): 142 | breadcrumbs = DemoBaseBreadcrumbs() 143 | breadcrumbs.add_breadcrumb("Apex Charts") 144 | return breadcrumbs 145 | 146 | 147 | class TableView(DemoStandardMixin, TemplateView): 148 | menu_item = "Tables" 149 | 150 | def get_content(self): 151 | layout = WidgetListLayout("Tables", css_class="mt-4") 152 | layout.add_widget(DataTableCard(), "Basic Static Table", "table") 153 | layout.add_widget(TestTableCard(), "AJAX Model Table", "table") 154 | layout.add_widget(BookModelTableCard(), "Basic Model Table", "table") 155 | layout.add_widget(WidgetCellTableCard(), "Widget Cell Table", "table") 156 | return layout 157 | 158 | def get_breadcrumbs(self): 159 | breadcrumbs = DemoBaseBreadcrumbs() 160 | breadcrumbs.add_breadcrumb("Tables") 161 | return breadcrumbs 162 | 163 | 164 | class TextWidgetView(DemoStandardMixin, TemplateView): 165 | menu_item = "Text Widgets" 166 | 167 | def get_content(self): 168 | layout = WidgetListLayout("Text Widgets", css_class="mt-4") 169 | layout.add_widget(MarkdownCard(), "Markdown Widget", "file-text") 170 | layout.add_widget(HTMLCard(), "HTML Widget", "file-code") 171 | layout.add_widget(CodeCard(), "Code Widget", "file-binary") 172 | layout.add_widget(StringCard()) 173 | return layout 174 | 175 | def get_breadcrumbs(self): 176 | breadcrumbs = DemoBaseBreadcrumbs() 177 | breadcrumbs.add_breadcrumb("Text Widgets") 178 | return breadcrumbs 179 | 180 | 181 | class ListWidgetView(DemoStandardMixin, TemplateView): 182 | menu_item = "List Widgets" 183 | 184 | def get_content(self): 185 | layout = WidgetListLayout("List Widgets", css_class="mt-4") 186 | layout.add_widget(PagedBookCard()) 187 | layout.add_widget(AuthorListCard()) 188 | layout.add_widget(AuthorListModelCardWidgetCard()) 189 | return layout 190 | 191 | def get_breadcrumbs(self): 192 | breadcrumbs = DemoBaseBreadcrumbs() 193 | breadcrumbs.add_breadcrumb("List Widgets") 194 | return breadcrumbs 195 | 196 | 197 | class StructureWidgetView(DemoStandardMixin, TemplateView): 198 | menu_item = "Structure Widgets" 199 | 200 | def get_content(self): 201 | layout = WidgetListLayout("Structure Widgets", css_class="mt-4") 202 | layout.add_widget(TabCard(), "Tab Widget", "folder") 203 | layout.add_widget(CardCard(), "Card Widget", "card-heading") 204 | layout.add_widget(CrispyFormCard(), "Crispy Form Widget", "ui-checks") 205 | layout.add_widget(CollapseCard(), "Collapse Widget", "arrows-collapse") 206 | layout.add_widget( 207 | HorizontalLayoutCard(), "Horizontal Layout Widget", "grid-3x2-gap" 208 | ) 209 | return layout 210 | 211 | def post(self, request, *args, **kwargs): 212 | return HttpResponseRedirect(reverse("core:structure")) 213 | 214 | def get_breadcrumbs(self): 215 | breadcrumbs = DemoBaseBreadcrumbs() 216 | breadcrumbs.add_breadcrumb("Structure Widgets") 217 | return breadcrumbs 218 | 219 | 220 | class ModalView(DemoStandardMixin, TemplateView): 221 | menu_item = "Modal Widgets" 222 | 223 | def get_content(self): 224 | layout = WidgetListLayout("Modal Widget", css_class="mt-4") 225 | layout.add_widget(ModalCard(), "Modal Widget", "window-fullscreen") 226 | layout.add_widget( 227 | CrispyFormModalCard(), "Crispy Form Modal Widget", "ui-checks" 228 | ) 229 | return layout 230 | 231 | def post(self, request, *args, **kwargs): 232 | return HttpResponseRedirect(reverse("core:modal")) 233 | 234 | def get_breadcrumbs(self): 235 | breadcrumbs = DemoBaseBreadcrumbs() 236 | breadcrumbs.add_breadcrumb("Modal Widgets") 237 | return breadcrumbs 238 | -------------------------------------------------------------------------------- /wildewidgets/views/json.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import importlib 4 | from pathlib import Path 5 | from typing import Any 6 | 7 | from django.apps import apps 8 | from django.http import Http404, HttpRequest, HttpResponseBase, JsonResponse 9 | from django.views.generic import View 10 | from django.views.generic.base import TemplateView 11 | 12 | from .mixins import JSONResponseMixin, WidgetInitKwargsMixin 13 | 14 | 15 | class JSONResponseView(JSONResponseMixin, TemplateView): # type: ignore[misc] 16 | """ 17 | A view that renders a template and returns the result as a JSON response. 18 | 19 | This view combines Django's TemplateView with JSONResponseMixin to provide a 20 | simple way to return template-rendered content within a JSON response. This 21 | is useful for AJAX requests that need to return HTML fragments within a JSON 22 | structure. 23 | 24 | When subclassing, you typically need to override the template_name attribute 25 | and optionally the get_context_data method. 26 | 27 | Attributes: 28 | template_name: The template to render (must be set by subclasses) 29 | 30 | Example: 31 | .. code-block:: python 32 | 33 | class MyJSONView(JSONResponseView): 34 | template_name = "my_template.html" 35 | 36 | def get_context_data(self, **kwargs): 37 | context = super().get_context_data(**kwargs) 38 | context['extra_data'] = get_some_data() 39 | return context 40 | 41 | """ 42 | 43 | 44 | class JSONDataView(View): 45 | """ 46 | A view that returns JSON data in response to HTTP requests. 47 | 48 | This class provides a simple framework for views that need to return JSON data. 49 | It handles the HTTP request/response cycle and serializes the context data 50 | to JSON. By default, it only responds to GET requests. 51 | 52 | To use this class, subclass it and override the get_context_data method to 53 | provide the data you want to return as JSON. 54 | 55 | Example: 56 | .. code-block:: python 57 | 58 | class UserDataView(JSONDataView): 59 | def get_context_data(self, **kwargs): 60 | user_id = self.kwargs.get('user_id') 61 | user = User.objects.get(id=user_id) 62 | return { 63 | 'username': user.username, 64 | 'email': user.email, 65 | 'date_joined': user.date_joined.isoformat() 66 | } 67 | 68 | """ 69 | 70 | def get(self, request: HttpRequest, *args, **kwargs) -> JsonResponse: # noqa: ARG002 71 | """ 72 | Handle GET requests by returning JSON data. 73 | 74 | This method retrieves the context data and returns it as a JSON response. 75 | 76 | Args: 77 | request: The HTTP request object 78 | *args: Additional positional arguments 79 | **kwargs: Additional keyword arguments 80 | 81 | Returns: 82 | JsonResponse: HTTP response containing the JSON-serialized context data 83 | 84 | """ 85 | context = self.get_context_data() 86 | return self.render_to_response(context) 87 | 88 | def get_context_data(self, **kwargs) -> dict[str, Any]: # noqa: ARG002 89 | """ 90 | Get the data to include in the JSON response. 91 | 92 | Override this method to provide the data you want to include in the response. 93 | By default, it returns an empty dictionary. 94 | 95 | Args: 96 | **kwargs: Additional context data 97 | 98 | Returns: 99 | dict: The data to serialize to JSON 100 | 101 | """ 102 | return {} 103 | 104 | def render_to_response(self, context, **response_kwargs) -> JsonResponse: # noqa: ARG002 105 | """ 106 | Create a JSON response from the context data. 107 | 108 | This method serializes the context data to JSON and returns it as an 109 | HTTP response. 110 | 111 | Args: 112 | context: The data to serialize to JSON 113 | **response_kwargs: Additional response parameters (unused) 114 | 115 | Returns: 116 | JsonResponse: HTTP response containing the JSON-serialized context data 117 | 118 | """ 119 | return JsonResponse(context) 120 | 121 | 122 | class WildewidgetDispatch(WidgetInitKwargsMixin, View): 123 | """ 124 | A view that dynamically dispatches requests to widget classes. 125 | 126 | This view acts as a central dispatcher for widget AJAX requests. It examines 127 | the request parameters to determine which widget class to instantiate, then 128 | delegates the request handling to that widget's dispatch method. 129 | 130 | The view searches for widget classes in all installed Django apps by looking 131 | for a 'wildewidgets.py' file or a 'wildewidgets' directory within each app. 132 | 133 | This approach allows widgets to handle their own AJAX requests without 134 | requiring explicit URL routing for each widget type. 135 | 136 | Example usage in URLs: 137 | .. code-block:: python 138 | 139 | path( 140 | 'wildewidget/', 141 | WildewidgetDispatch.as_view(), 142 | name='wildewidget_dispatch' 143 | ) 144 | 145 | Example client-side code: 146 | .. code-block:: javascript 147 | 148 | $.ajax({ 149 | url: '/wildewidget/', 150 | data: { 151 | 'wildewidgetclass': 'MyWidget', 152 | 'extra_data': encodeURIComponent(JSON.stringify({ 153 | args: [], 154 | kwargs: {param1: 'value1'} 155 | })) 156 | } 157 | }); 158 | """ 159 | 160 | def dispatch( # type: ignore[override] 161 | self, request: HttpRequest, *args, **kwargs 162 | ) -> HttpResponseBase | Http404: 163 | """ 164 | Dispatch the request to the appropriate widget class. 165 | 166 | This method: 167 | 168 | 1. Extracts the widget class name from the request 169 | 2. Searches for the widget class in all installed apps 170 | 3. Instantiates the widget class with the provided arguments 171 | 4. Delegates to the widget's dispatch method 172 | 173 | Args: 174 | request: The HTTP request object 175 | *args: Additional positional arguments 176 | 177 | Keyword Arguments: 178 | **kwargs: Additional keyword arguments 179 | 180 | Returns: 181 | HttpResponseBase: The response from the widget's dispatch method 182 | Http404: If the widget class cannot be found 183 | 184 | Note: 185 | The request must include: 186 | - 'wildewidgetclass': The name of the widget class to instantiate 187 | 188 | The request may include: 189 | - 'csrf_token': Optional CSRF token for protected requests 190 | - 'extra_data': Optional base64 encoded JSON encoded string 191 | containing a dict with 'args' and 'kwargs' keys for widget 192 | initialization 193 | 194 | """ 195 | wildewidgetclass = request.GET.get("wildewidgetclass", None) 196 | csrf_token = request.GET.get("csrf_token", "") 197 | if wildewidgetclass: 198 | configs = apps.get_app_configs() 199 | for config in configs: 200 | check_file = Path(config.path) / "wildewidgets.py" 201 | check_dir = Path(config.path) / "wildewidgets" 202 | if check_file.is_file() or check_dir.is_dir(): 203 | module = importlib.import_module(f"{config.name}.wildewidgets") 204 | if hasattr(module, wildewidgetclass): 205 | class_ = getattr(module, wildewidgetclass) 206 | extra_data = self.get_decoded_extra_data(request) 207 | initargs = extra_data.get("args", []) 208 | initkwargs = extra_data.get("kwargs", {}) 209 | instance = class_(*initargs, **initkwargs) 210 | instance.request = request 211 | instance.csrf_token = csrf_token 212 | instance.args = initargs 213 | instance.kwargs = initkwargs 214 | return instance.dispatch(request, *args, **kwargs) 215 | return Http404("Not Found: Wildewidget class not found") 216 | --------------------------------------------------------------------------------