├── .autoenv ├── .autoenv.leave ├── .bumpversion.cfg ├── .gitignore ├── .readthedocs.yaml ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.md ├── bin └── release.sh ├── demo ├── .dockerignore ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── bin │ ├── collectstatic.sh │ ├── restart-gunicorn.sh │ └── wait-for-it.sh ├── demo │ ├── __init__.py │ ├── core │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── fixtures │ │ │ ├── book_manager.json │ │ │ ├── measurements.json │ │ │ └── users.json │ │ ├── forms.py │ │ ├── jobs.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_initial_data.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── static │ │ │ └── core │ │ │ │ └── images │ │ │ │ └── dark_logo.png │ │ ├── templates │ │ │ └── core │ │ │ │ └── intermediate.html │ │ ├── templatetags │ │ │ └── __init__.py │ │ ├── tests.py │ │ ├── urls.py │ │ ├── views.py │ │ └── wildewidgets.py │ ├── gunicorn_config.py │ ├── settings.py │ ├── settings_docker.py │ ├── urls.py │ ├── users │ │ ├── __init__.py │ │ ├── apps.py │ │ ├── fixtures │ │ │ └── users.json │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_load_fixture.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── templates │ │ │ ├── registration │ │ │ │ └── login.html │ │ │ └── users │ │ │ │ └── intermediate.html │ │ └── tests.py │ └── wsgi.py ├── docker-compose.yml ├── etc │ ├── environment.txt │ ├── gunicorn_logging.conf │ ├── ipython_config.py │ ├── nginx.conf │ └── supervisord.conf ├── manage.py ├── requirements.txt ├── setup.cfg ├── setup.py └── sql │ └── docker │ ├── init.sql │ └── my.cnf ├── docs ├── Makefile ├── _static │ ├── favicon.ico │ ├── wildewidgets.png │ ├── wildewidgets_dark_mode_logo.png │ ├── wildewidgets_dark_mode_logo.pxd │ ├── wildewidgets_logo.png │ └── wildewidgets_logo.pxd ├── api.rst ├── api_base.rst ├── api_buttons.rst ├── api_charts.rst ├── api_datagrid.rst ├── api_forms.rst ├── api_grid.rst ├── api_headers.rst ├── api_icons.rst ├── api_layout.rst ├── api_misc.rst ├── api_modals.rst ├── api_navigation.rst ├── api_structure.rst ├── api_tables.rst ├── api_text.rst ├── api_views.rst ├── business_charts.rst ├── charts.rst ├── conf.py ├── demo_ss_home.png ├── demo_ss_simple_table.png ├── favicon.ico ├── guide.rst ├── index.rst ├── install.rst ├── make.bat ├── requirements.txt ├── scientific_charts.rst ├── tables.rst └── widgets.rst ├── pyproject.toml ├── requirements.txt ├── setup.cfg ├── setup.py ├── uv.lock └── wildewidgets ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── menus.py ├── models.py ├── settings.py ├── static └── wildewidgets │ ├── css │ ├── _navbar.scss │ ├── _toggleablemanytomanyfieldblock.scss │ ├── _widget-index.scss │ ├── _widget-list.scss │ ├── highlighting.css │ ├── table_extra.css │ ├── wildewidgets.css │ └── wildewidgets.scss │ ├── images │ └── placeholder.png │ └── js │ └── wildewidgets.js ├── templates └── wildewidgets │ ├── altairchart.html │ ├── apex_chart.html │ ├── apex_json.html │ ├── barchart.html │ ├── block--simple.html │ ├── block.html │ ├── button--form.html │ ├── card_block.html │ ├── categorychart.html │ ├── crispy_form_modal.html │ ├── crispy_form_widget.html │ ├── doughnutchart.html │ ├── doughnutchart_json.html │ ├── header_with_collapse_button.html │ ├── header_with_controls.html │ ├── header_with_link_button.html │ ├── header_with_modal_button.html │ ├── header_with_widget.html │ ├── html_widget.html │ ├── initials_avatar.html │ ├── link_button.html │ ├── list_model_widget.html │ ├── markdown_widget.html │ ├── menu.html │ ├── modal.html │ ├── page_tab_block.html │ ├── paged_model_widget.html │ ├── stackedbarchart.html │ ├── stackedbarchart_json.html │ ├── tab_block.html │ ├── table.html │ ├── widget-list--main.html │ ├── widget-list--sidebar.html │ ├── widget-list.html │ ├── widget_index.html │ └── widget_stream.html ├── templatetags ├── __init__.py └── wildewidgets.py ├── tests.py ├── views ├── __init__.py ├── generic.py ├── json.py ├── mixins.py ├── permission.py └── tables.py ├── viewsets.py └── widgets ├── __init__.py ├── base.py ├── buttons.py ├── charts ├── __init__.py ├── altair.py ├── apex.py └── chartjs.py ├── datagrid.py ├── forms.py ├── grid.py ├── headers.py ├── icons.py ├── layout.py ├── misc.py ├── modals.py ├── navigation.py ├── structure.py ├── tables ├── __init__.py ├── actions.py ├── base.py ├── components.py ├── tables.py └── views.py └── text.py /.autoenv: -------------------------------------------------------------------------------- 1 | if [[ -f .venv/bin/activate ]]; then 2 | source .venv/bin/activate 3 | fi 4 | -------------------------------------------------------------------------------- /.autoenv.leave: -------------------------------------------------------------------------------- 1 | CWD=$(pwd) 2 | if [[ ! $CWD == *"$VIRTUAL_ENV_PROMPT"* ]]; then 3 | deactivate 4 | fi 5 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.1.4 3 | commit = True 4 | tag = True 5 | tag_name = {new_version} 6 | 7 | [bumpversion:file:setup.py] 8 | 9 | [bumpversion:file:pyproject.toml] 10 | 11 | [bumpversion:file:wildewidgets/__init__.py] 12 | 13 | [bumpversion:file:docs/conf.py] 14 | 15 | [bumpversion:file:demo/setup.py] 16 | 17 | [bumpversion:file:demo/Makefile] 18 | 19 | [bumpversion:file:demo/demo/__init__.py] 20 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | image: latest 7 | apt_packages: 8 | - gcc 9 | - libssl1.1 10 | - libssl-dev 11 | - libmysqlclient-dev 12 | 13 | sphinx: 14 | configuration: docs/conf.py 15 | 16 | formats: all 17 | 18 | python: 19 | version: 3.8 20 | install: 21 | - requirements: requirements.txt 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | recursive-include wildewidgets/static * 4 | recursive-include wildewidgets/templates * 5 | -------------------------------------------------------------------------------- /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 | tox: 21 | # create a tox pyenv virtualenv based on 2.7.x 22 | # install tox and tox-pyenv in that ve 23 | # actiave that ve before running this 24 | @tox 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | echo "Pushing main to origin ..." 15 | git push --tags origin main 16 | echo "Uploading to PyPI ..." 17 | twine upload dist/* 18 | else 19 | echo "Last commit was not a bumpversion; aborting." 20 | echo "Last commit message: ${MSG}" 21 | fi 22 | else 23 | git status 24 | echo 25 | echo 26 | echo "------------------------------------------------------" 27 | echo "You have uncommitted changes; aborting." 28 | echo "------------------------------------------------------" 29 | fi 30 | else 31 | echo "You're not on main; aborting." 32 | fi 33 | -------------------------------------------------------------------------------- /demo/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .idea 3 | 4 | 5 | /*.sql 6 | 7 | tags 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/m3v9w5i2/caltech-imss-ads/amazonlinux2-python3.10 2 | 3 | USER root 4 | 5 | ENV LC_ALL=en_US.utf8 LANG=en_US.utf8 PYCURL_SSL_LIBRARY=nss 6 | 7 | RUN yum -y update && \ 8 | yum -y install \ 9 | gcc \ 10 | glibc-locale-source \ 11 | glibc-langpack-en \ 12 | git \ 13 | libcurl-devel \ 14 | openssl-devel \ 15 | # Even though we don't always use Apache in these apps, mod_ssl installs certs that we need. 16 | mod_ssl \ 17 | # We need these mysql packages for python to be able to connect yo mysql, and for `manage.py dbshell` to work. 18 | mysql-devel \ 19 | mysql \ 20 | # Useful unix utilities that don't come with the base CentOS 7 image. 21 | hostname \ 22 | procps \ 23 | psmisc \ 24 | which \ 25 | # Cleanup for yum, as suggested by yum itself. 26 | && yum -y clean all && \ 27 | rm -rf /var/cache/yum && \ 28 | # Install nginx from amazon-linux-extras -- you can't get it through straight yum 29 | amazon-linux-extras install nginx1 && \ 30 | # Set up the UTF-8 locale so that shelling into the container won't spam you with locale errors. 31 | localedef -i en_US -f UTF-8 en_US.UTF-8 && \ 32 | # Install supervisor globally 33 | /opt/python/bin/pip3.10 install supervisor && \ 34 | # Create the virtualenv. Note that we NEED to use `python3`, here, since `python` is currently py2.7. Once we set 35 | # up the PATH on the next command, `python` becomes py3.6. 36 | /opt/python/bin/python3.10 -m venv /ve && \ 37 | # Add the user under which gunicorn will run. 38 | adduser -r gunicorn 39 | 40 | # Ensure that we run the pip and python that are in the virtualenv, rather than the system copies. 41 | ENV PATH /ve/bin:/opt/python/3.10/bin:$PATH 42 | # Install the latest pip and our dependencies into the virtualenv. We do this 43 | # before copying the codebase so that minor code changes don't force a rebuild 44 | # of the entire virtualenv. 45 | COPY requirements.txt /tmp/requirements.txt 46 | COPY django-wildewidgets.tar.gz /tmp/django-wildewidgets.tar.gz 47 | RUN pip install --upgrade pip wheel && \ 48 | pip install -r /tmp/requirements.txt && \ 49 | pip install /tmp/django-wildewidgets.tar.gz && \ 50 | # Purge the pip cache, which can save us a good few megabytes in the docker image. 51 | rm -rf `pip cache dir` 52 | 53 | COPY . /app 54 | WORKDIR /app 55 | 56 | RUN pip install --upgrade pip && \ 57 | pip install -r requirements.txt 58 | 59 | # Install our app into the ve, symlink static resources, copy settings files and certs into place, etc. 60 | RUN pip install -e . && \ 61 | # precompile the sass files 62 | pip3 install django-compressor && \ 63 | python manage.py compilescss --settings=demo.settings_docker -v0 --skip-checks && \ 64 | # Run collectstatic to symlink all the static files into /static, which is where the webserver expects them. 65 | python manage.py collectstatic --settings=demo.settings_docker --noinput -v0 --link && \ 66 | chown -R nginx:nginx /static && \ 67 | # Uncomment this if your project requires internationalization. 68 | # python manage.py compilemessages --settings=demo.settings_docker && \ 69 | # Copy/create/delete various config files and folders. 70 | cp etc/supervisord.conf /etc/supervisord.conf && \ 71 | cp etc/nginx.conf /etc/nginx/nginx.conf && \ 72 | cp etc/gunicorn_logging.conf /etc/gunicorn_logging.conf && \ 73 | # generate the default certs 74 | /usr/libexec/httpd-ssl-gencerts && \ 75 | mkdir /certs && \ 76 | cp /etc/pki/tls/private/localhost.key /certs && \ 77 | cp /etc/pki/tls/certs/localhost.crt /certs && \ 78 | chmod a+r /certs/* && \ 79 | # Create an iPython profile and then replace the default config with our own, which enables automatic code reload. 80 | ipython profile create && \ 81 | cp etc/ipython_config.py /root/.ipython/profile_default/ipython_config.py 82 | 83 | # Expose the ports we configured gunicorn to listen to. 84 | EXPOSE 443 85 | 86 | CMD ["/opt/python/bin/supervisord", "-c", "/etc/supervisord.conf"] 87 | -------------------------------------------------------------------------------- /demo/Makefile: -------------------------------------------------------------------------------- 1 | VERSION = 1.1.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 | -------------------------------------------------------------------------------- /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/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/bin/restart-gunicorn.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | killall -HUP gunicorn 3 | -------------------------------------------------------------------------------- /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/demo/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.1.4" 2 | -------------------------------------------------------------------------------- /demo/demo/core/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'demo.core.apps.CoreConfig' 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo/demo/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | name = 'demo.core' 6 | label = 'core' 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo/demo/core/forms.py: -------------------------------------------------------------------------------- 1 | #################################### 2 | # Define your core app's forms here. 3 | #################################### 4 | -------------------------------------------------------------------------------- /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 user-facing views. 4 | ############################################################################################################ 5 | -------------------------------------------------------------------------------- /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/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/demo/core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/a1065ef39de1605d7304d4208d17404683beab21/demo/demo/core/migrations/__init__.py -------------------------------------------------------------------------------- /demo/demo/core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class Measurement(models.Model): 4 | name = models.CharField(max_length=128) 5 | time = models.DateTimeField( 6 | 'Time', 7 | help_text='The date and time the measurement was made.' 8 | ) 9 | pressure = models.DecimalField(max_digits=8, decimal_places=2) 10 | temperature = models.DecimalField(max_digits=8, decimal_places=2) 11 | restricted = models.BooleanField(default=False) 12 | open = models.BooleanField(default=True) 13 | -------------------------------------------------------------------------------- /demo/demo/core/static/core/images/dark_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/a1065ef39de1605d7304d4208d17404683beab21/demo/demo/core/static/core/images/dark_logo.png -------------------------------------------------------------------------------- /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 %} -------------------------------------------------------------------------------- /demo/demo/core/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/a1065ef39de1605d7304d4208d17404683beab21/demo/demo/core/templatetags/__init__.py -------------------------------------------------------------------------------- /demo/demo/core/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /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/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' 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 | # If remote debugging is enabled set the timeout very high, so one can pause for a long time in the debugger. 21 | # Also set the number of workers to 1, which improves the debugging experience by not overwhleming the remote debugger. 22 | if env.bool('REMOTE_DEBUG_ENABLED', default=False): 23 | timeout = 9999 24 | workers = 1 25 | 26 | # Logging. 27 | accesslog = '-' 28 | access_log_format = '%({X-Forwarded-For}i)s %(l)s %(u)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s' 29 | errorlog = '-' 30 | syslog = False 31 | 32 | # statsd settings need to have defaults provided, because dev sites don't use statsd. 33 | host = env('STATSD_HOST', default=None) 34 | port = env.int('STATSD_PORT', default=8125) 35 | if host and port: 36 | statsd_host = "{}:{}".format(host, port) 37 | else: 38 | statsd_host = None 39 | statsd_prefix = env('STATSD_PREFIX', default=None) 40 | -------------------------------------------------------------------------------- /demo/demo/settings_docker.py: -------------------------------------------------------------------------------- 1 | import os 2 | # These are the env vars that get retrieved with getenv(). They must be set to avoid raising UnsetEnvironmentVariable. 3 | os.environ['DB_NAME'] = 'fake' 4 | os.environ['DB_USER'] = 'fake' 5 | os.environ['DB_PASSWORD'] = 'fake' 6 | os.environ['DB_HOST'] = 'fake' 7 | os.environ['DB_PORT'] = 'fake' 8 | os.environ['CACHE'] = 'fake' 9 | # These are set to True here ONLY so the static resouces for django-debug-toolbar will be collected during image build. 10 | os.environ['DEVELOPMENT'] = 'True' 11 | os.environ['ENABLE_DEBUG_TOOLBAR'] = 'True' 12 | 13 | # noinspection PyUnresolvedReferences 14 | from .settings import * # noqa F403 15 | -------------------------------------------------------------------------------- /demo/demo/urls.py: -------------------------------------------------------------------------------- 1 | """demo URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | from django.conf import settings 19 | 20 | from wildewidgets import WildewidgetDispatch 21 | 22 | from .core import urls as core_urls 23 | 24 | 25 | urlpatterns = [ 26 | # This setup assumes that you're making an access.caltech app, which needs the app name as a prefix for all URLs. 27 | # If that's not true, you can change this to whatever base path you want, including ''. 28 | path('wildewidgets_json', WildewidgetDispatch.as_view(), name='wildewidgets_json'), 29 | path('', include(core_urls, namespace='core')), 30 | ] 31 | -------------------------------------------------------------------------------- /demo/demo/users/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'demo.users.apps.UsersConfig' 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo/demo/users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/a1065ef39de1605d7304d4208d17404683beab21/demo/demo/users/migrations/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/users/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /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 | import os 10 | import sys 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | # This allows easy placement of apps within the interior demo directory. 15 | app_path = os.path.abspath( 16 | os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir) 17 | ) 18 | sys.path.append(os.path.join(app_path, "demo")) 19 | 20 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 21 | # if running multiple sites in the same mod_wsgi process. To fix this, use 22 | # mod_wsgi daemon mode with each site in its own daemon process, or use 23 | # os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.production" 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 | # from helloworld.wsgi import HelloWorldApplication 31 | # application = HelloWorldApplication(application) 32 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo/etc/nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | user nginx; 3 | worker_processes 1; 4 | daemon off; 5 | 6 | error_log /dev/stderr info; 7 | pid /tmp/nginx.pid; 8 | 9 | events { 10 | worker_connections 1024; 11 | } 12 | 13 | 14 | http { 15 | include /etc/nginx/mime.types; 16 | default_type application/octet-stream; 17 | 18 | log_format main 'APACHE_ACCESS $http_x_forwarded_for - $remote_user [$time_iso8601] "$request" ' 19 | '$status $body_bytes_sent "$http_referer" "$http_user_agent"'; 20 | 21 | access_log /dev/stdout main; 22 | 23 | #sendfile on; 24 | # sendfile is turned off here because, in virtualbox, sendfile on corrupts 25 | # javascript files somehow -- it causes nginx to leave off the last few bytes 26 | # of the file 27 | sendfile off; 28 | 29 | server { 30 | listen 443 ssl; 31 | server_name localhost; 32 | keepalive_timeout 305; 33 | 34 | # this makes nginx stop complaining about our access.caltech headers 35 | underscores_in_headers on; 36 | 37 | ssl_certificate /certs/localhost.crt; 38 | ssl_certificate_key /certs/localhost.key; 39 | 40 | # We've configured the ALB Target Group health check to look at /lb-status 41 | location /lb-status { 42 | return 200 'Hello, Mr. Load balancer.'; 43 | add_header Content-Type text/plain; 44 | } 45 | 46 | location = /favicon.ico { access_log off; log_not_found off; } 47 | 48 | location ^~ /static/demo/ { 49 | gzip_static on; 50 | expires max; 51 | add_header Cache-Control public; 52 | alias /static/; 53 | } 54 | 55 | location / { 56 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 57 | proxy_set_header Host $http_host; 58 | proxy_set_header X-Forwarded_Proto $scheme; 59 | proxy_redirect off; 60 | proxy_read_timeout 305s; 61 | proxy_send_timeout 305s; 62 | proxy_pass http://unix:/tmp/demo.sock; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /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 | 10 | [program:gunicorn] 11 | command=gunicorn --ssl-version 2 --config /app/demo/gunicorn_config.py demo.wsgi 12 | user=gunicorn 13 | directory=/app 14 | stdout_logfile=/dev/stdout 15 | stdout_logfile_maxbytes=0 16 | redirect_stderr=true 17 | 18 | [program:nginx] 19 | command=/usr/sbin/nginx -c /etc/nginx/nginx.conf 20 | stdout_logfile=/dev/stdout 21 | stdout_logfile_maxbytes=0 22 | redirect_stderr=true 23 | 24 | [rpcinterface:supervisor] 25 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 26 | 27 | [supervisorctl] 28 | serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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==4.1.3 # 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==1.13.0 # https://github.com/django-crispy-forms/django-crispy-forms 9 | crispy-bootstrap5==0.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.9.1 # 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-generic-json-views==0.8 # https://github.com/bbengfort/django-generic-json-views 18 | django-theme-academy==0.3.2 # https://github.com/caltechads/django-theme-academy 19 | django-book-manager==0.3.2 # https://github.com/caltechads/django-book-manager 20 | 21 | # Other utils 22 | # ------------------------------------------------------------------------------ 23 | bleach==5.0.1 # https://github.com/mozilla/bleach 24 | altair==4.2.0 # https://github.com/altair-viz/altair 25 | colorama==0.4.6 # https://github.com/tartley/colorama 26 | crython==0.2.0 # https://github.com/ahawker/crython 27 | ipython==8.6.0 # https://github.com/ipython/ipython 28 | mysqlclient==2.1.1 # https://github.com/PyMySQL/mysqlclient-python 29 | pytz==2022.6 # https://github.com/stub42/pytz 30 | structlog==22.2.0 # https://github.com/hynek/structlog 31 | # --- SASS Processing 32 | django-sass-processor==1.2.2 # https://github.com/jrief/django-sass-processor 33 | libsass==0.22.0 34 | 35 | # Web server 36 | # ------------------------------------------------------------------------------ 37 | gunicorn==20.1.0 # https://github.com/benoitc/gunicorn 38 | 39 | 40 | # Deployment 41 | # ------------------------------------------------------------------------------ 42 | bumpversion==0.6.0 # https://github.com/peritus/bumpversion 43 | 44 | 45 | # # Development 46 | # # ------------------------------------------------------------------------------ 47 | # django-debug-toolbar==2.2 # https://github.com/jazzband/django-debug-toolbar 48 | # django-debug-toolbar-template-profiler==2.0.1 # https://github.com/node13h/django-debug-toolbar-template-profiler 49 | # django-queryinspect==1.1.0 # https://github.com/dobarkod/django-queryinspect 50 | # django-extensions==2.2.9 # https://github.com/django-extensions/django-extensions 51 | # autopep8==1.5 # https://github.com/hhatto/autopep8 52 | # flake8==3.7.9 # https://github.com/PyCQA/flake8 53 | # pycodestyle==2.5.0 # https://github.com/PyCQA/pycodestyle 54 | 55 | # # Testing 56 | # # ------------------------------------------------------------------------------ 57 | # coverage==5.0.3 # https://github.com/nedbat/coveragepy 58 | # django-coverage-plugin==1.8.0 # https://github.com/nedbat/django_coverage_plugin 59 | # factory-boy==2.12.0 # https://github.com/FactoryBoy/factory_boy 60 | # mypy==0.701 # https://github.com/python/mypy 61 | # testfixtures==6.14.0 # https://github.com/Simplistix/testfixtures 62 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup, find_packages 3 | 4 | setup( 5 | name="demo", 6 | version="1.1.4", 7 | description="", 8 | author="Caltech IMSS ADS", 9 | author_email="imss-ads-staff@caltech.edu", 10 | packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests", "htmlcov"]) 11 | ) 12 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/a1065ef39de1605d7304d4208d17404683beab21/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/_static/wildewidgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/a1065ef39de1605d7304d4208d17404683beab21/docs/_static/wildewidgets.png -------------------------------------------------------------------------------- /docs/_static/wildewidgets_dark_mode_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/a1065ef39de1605d7304d4208d17404683beab21/docs/_static/wildewidgets_dark_mode_logo.png -------------------------------------------------------------------------------- /docs/_static/wildewidgets_dark_mode_logo.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/a1065ef39de1605d7304d4208d17404683beab21/docs/_static/wildewidgets_dark_mode_logo.pxd -------------------------------------------------------------------------------- /docs/_static/wildewidgets_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/a1065ef39de1605d7304d4208d17404683beab21/docs/_static/wildewidgets_logo.png -------------------------------------------------------------------------------- /docs/_static/wildewidgets_logo.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/a1065ef39de1605d7304d4208d17404683beab21/docs/_static/wildewidgets_logo.pxd -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/api_base.rst: -------------------------------------------------------------------------------- 1 | Base Widgets 2 | ============ 3 | 4 | 5 | .. automodule:: wildewidgets.widgets.base 6 | :members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/api_buttons.rst: -------------------------------------------------------------------------------- 1 | Button Widgets 2 | ============== 3 | 4 | 5 | .. automodule:: wildewidgets.widgets.buttons 6 | :members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /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: -------------------------------------------------------------------------------- /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: -------------------------------------------------------------------------------- /docs/api_grid.rst: -------------------------------------------------------------------------------- 1 | Boostrap Grid 2 | ============= 3 | 4 | 5 | .. automodule:: wildewidgets.widgets.grid 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: -------------------------------------------------------------------------------- /docs/api_icons.rst: -------------------------------------------------------------------------------- 1 | Icons 2 | ===== 3 | 4 | 5 | .. automodule:: wildewidgets.widgets.icons 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_misc.rst: -------------------------------------------------------------------------------- 1 | Misc Widgets 2 | ============ 3 | 4 | 5 | .. automodule:: wildewidgets.widgets.misc 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: -------------------------------------------------------------------------------- /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: -------------------------------------------------------------------------------- /docs/api_structure.rst: -------------------------------------------------------------------------------- 1 | Structure Widgets 2 | ================= 3 | 4 | 5 | .. automodule:: wildewidgets.widgets.structure 6 | :members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /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: -------------------------------------------------------------------------------- /docs/api_text.rst: -------------------------------------------------------------------------------- 1 | Text Widgets 2 | ============ 3 | 4 | 5 | .. automodule:: wildewidgets.widgets.text 6 | :members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/charts.rst: -------------------------------------------------------------------------------- 1 | Charts 2 | ------ 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | business_charts 9 | scientific_charts 10 | 11 | -------------------------------------------------------------------------------- /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.1.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 | -------------------------------------------------------------------------------- /docs/demo_ss_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/a1065ef39de1605d7304d4208d17404683beab21/docs/demo_ss_home.png -------------------------------------------------------------------------------- /docs/demo_ss_simple_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/a1065ef39de1605d7304d4208d17404683beab21/docs/demo_ss_simple_table.png -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/a1065ef39de1605d7304d4208d17404683beab21/docs/favicon.ico -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "django-wildewidgets" 3 | version = "1.1.4" 4 | description = """django-wildewidgets is a Django design library providing several tools for 5 | building full-featured, widget-based web applications with a standard, 6 | consistent design, based on Bootstrap.""" 7 | readme = "README.md" 8 | requires-python = ">=3.7" 9 | authors = [ 10 | {name = "Caltech IMSS ADS", email = "imss-ads-staff@caltech.edu"} 11 | ] 12 | keywords = ["design", "widget", "django"] 13 | classifiers = [ 14 | "Development Status :: 4 - Beta", 15 | "Intended Audience :: Developers", 16 | "Operating System :: OS Independent", 17 | "Programming Language :: Python", 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3.7", 20 | "Programming Language :: Python :: 3.8", 21 | "Programming Language :: Python :: 3.9", 22 | "Programming Language :: Python :: 3.10", 23 | "Programming Language :: Python :: 3.11", 24 | "Framework :: Django", 25 | "Framework :: Django :: 3.2", 26 | "Framework :: Django :: 4.0", 27 | "Framework :: Django :: 4.1", 28 | "Framework :: Django :: 5.1", 29 | "Topic :: Software Development :: User Interfaces", 30 | ] 31 | dependencies = [ 32 | "django-crispy-forms", 33 | "crispy-bootstrap5", 34 | ] 35 | 36 | [project.urls] 37 | Homepage = "https://github.com/caltechads/django-wildewidgets" 38 | 39 | [build-system] 40 | requires = ["setuptools>=61.0"] 41 | build-backend = "setuptools.build_meta" 42 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | -r docs/requirements.txt 3 | -r demo/requirements.txt 4 | 5 | # Packaging 6 | # ------------------------------------------------------------------------------ 7 | bumpversion==0.6.0 # https://github.com/peritus/bumpversion 8 | twine # https://github.com/pypa/twine/ 9 | tox # https://github.com/tox-dev/to 10 | wheel # https://github.com/pypa/wheel 11 | 12 | # Development 13 | # ------------------------------------------------------------------------------ 14 | autopep8 # https://github.com/hhatto/autopep8 15 | flake8 # https://github.com/PyCQA/flake8 16 | pycodestyle # https://github.com/PyCQA/pycodestyle 17 | mypy # https://github.com/python/mypy 18 | django-stubs 19 | 20 | # Other utils 21 | # ------------------------------------------------------------------------------ 22 | ipython>=7.18.0 # https://github.com/ipython/ipython -------------------------------------------------------------------------------- /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 | [pylint.FORMAT] 13 | max-line-length=120 14 | 15 | [pylint.MESSAGES CONTROL] 16 | disable= 17 | missing-docstring, 18 | protected-access, 19 | unused-argument, 20 | invalid-name, 21 | too-few-public-methods, 22 | attribute-defined-outside-init, 23 | too-many-lines, 24 | no-member, 25 | unnecessary-pass 26 | 27 | [mypy] 28 | python_executable: ~/.pyenv/shims/python 29 | exclude = (?x)( 30 | ^build$ 31 | | demo\/setup\.py$ 32 | | migrations\/.*.py$ 33 | ) 34 | 35 | [mypy-setuptools.*] 36 | ignore_missing_imports = True 37 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "r", encoding='utf-8') as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | name='django-wildewidgets', 8 | version="1.1.4", 9 | packages=find_packages(exclude=['bin']), 10 | include_package_data=True, 11 | description='django-wildewidgets is a Django design library providing several tools for ' 12 | 'building full-featured, widget-based web applications with a standard, ' 13 | 'consistent design, based on Bootstrap.', 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | install_requires=[ 17 | 'django-crispy-forms', 18 | 'crispy-bootstrap5', 19 | ], 20 | author="Caltech IMSS ADS", 21 | author_email="imss-ads-staff@caltech.edu", 22 | url='https://github.com/caltechads/django-wildewidgets', 23 | keywords=['design', 'widget', 'django'], 24 | classifiers=[ 25 | 'Development Status :: 4 - Beta', 26 | 'Intended Audience :: Developers', 27 | 'Operating System :: OS Independent', 28 | 'Programming Language :: Python', 29 | 'Programming Language :: Python :: 3', 30 | 'Programming Language :: Python :: 3.7', 31 | 'Programming Language :: Python :: 3.8', 32 | 'Programming Language :: Python :: 3.9', 33 | 'Programming Language :: Python :: 3.10', 34 | 'Programming Language :: Python :: 3.11', 35 | "Framework :: Django", 36 | "Framework :: Django :: 3.2", 37 | "Framework :: Django :: 4.0", 38 | "Framework :: Django :: 4.1", 39 | "Topic :: Software Development :: User Interfaces", 40 | ], 41 | ) 42 | -------------------------------------------------------------------------------- /wildewidgets/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.1.4" 2 | 3 | from .widgets import * # noqa: F403,F401 4 | from .menus import * # noqa: F403,F401 5 | from .views import * # noqa: F403,F401 6 | -------------------------------------------------------------------------------- /wildewidgets/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /wildewidgets/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class WildewidgetsConfig(AppConfig): 5 | name = 'wildewidgets' 6 | -------------------------------------------------------------------------------- /wildewidgets/menus.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from django import template 4 | from django.urls import reverse 5 | 6 | from wildewidgets.views import WidgetInitKwargsMixin 7 | 8 | 9 | class BasicMenu(WidgetInitKwargsMixin): 10 | """ 11 | Basic menu widget. 12 | 13 | A basic menu requires only one class attribute defined, :py:attr:`items`:: 14 | 15 | class MainMenu(BasicMenu): 16 | 17 | items = [ 18 | ('Users', 'core:home'), 19 | ('Uploads','core:uploads'), 20 | ] 21 | 22 | """ 23 | 24 | template_file: str = "wildewidgets/menu.html" 25 | navbar_classes: str = "navbar-expand-lg navbar-light" 26 | container: str = "container-lg" 27 | brand_image = None 28 | brand_image_width: str = "100%" 29 | brand_text: str = None 30 | brand_url: str = "#" 31 | #: a list of tuples that define the items to list in the menu, where the 32 | #: first element is the menu item text and the second element is the URL 33 | #: name. A third optional element in the tuple can be a dictionary of get 34 | #: arguments 35 | items = [] 36 | 37 | def __init__(self, *args, **kwargs): # pylint: disable=super-init-not-called 38 | self.menu = {} 39 | self.active = None 40 | if args: 41 | self.active_hierarchy = args[0].split('/') 42 | else: 43 | self.active_hierarchy = [] 44 | 45 | def build_menu(self): 46 | if len(self.active_hierarchy) > 0: 47 | for item in self.items: 48 | data = {} 49 | if isinstance(item[1], str): 50 | data['url'] = reverse(item[1]) 51 | data['extra'] = '' 52 | data['kind'] = 'item' 53 | 54 | if len(item) > 2: 55 | extra = item[2] 56 | if isinstance(extra, dict): 57 | extra_list = [] 58 | for k, v in extra.items(): 59 | extra_list.append(f"{k}={v}") 60 | extra = f"?{'&'.join(extra_list)}" 61 | data['extra'] = extra 62 | elif isinstance(item[1], list): 63 | if len(self.active_hierarchy) > 1: 64 | submenu_active = self.active_hierarchy[1] 65 | else: 66 | submenu_active = None 67 | data = self.parse_submemu(item[1], submenu_active) 68 | 69 | self.add_menu_item(item[0], data, item[0] == self.active_hierarchy[0]) 70 | 71 | def add_menu_item(self, title, data, active=False): 72 | self.menu[title] = data 73 | if active: 74 | self.active = title 75 | 76 | def parse_submemu(self, items, submenu_active): 77 | data = { 78 | 'kind': 'submenu' 79 | } 80 | sub_menu_items = [] 81 | for item in items: 82 | if not isinstance(item, tuple): 83 | continue 84 | if item[0] == 'divider': 85 | subdata = { 86 | 'divider': True 87 | } 88 | else: 89 | subdata = { 90 | 'title': item[0], 91 | 'url': reverse(item[1]), 92 | 'extra': '', 93 | 'divider': False, 94 | 'active': item[0] == submenu_active, 95 | } 96 | 97 | if len(item) > 2: 98 | subdata['extra'] = self.convert_extra(item[2]) 99 | sub_menu_items.append(subdata) 100 | 101 | data['items'] = sub_menu_items 102 | return data 103 | 104 | def get_content(self, **kwargs): 105 | self.build_menu() 106 | context = { 107 | 'menu': self.menu, 108 | 'active': self.active, 109 | 'navbar_classes': self.navbar_classes, 110 | 'navbar_container': self.container, 111 | 'brand_image': self.brand_image, 112 | 'brand_image_width': self.brand_image_width, 113 | 'brand_text': self.brand_text, 114 | 'brand_url': self.brand_url, 115 | 'vertical': "navbar-vertical" in self.navbar_classes, 116 | 'target': random.randrange(0, 10000), 117 | } 118 | html_template = template.loader.get_template(self.template_file) 119 | content = html_template.render(context) 120 | return content 121 | 122 | def __str__(self): 123 | return self.get_content() 124 | 125 | 126 | class DarkMenu(BasicMenu): 127 | navbar_classes = "navbar-expand-lg navbar-dark bg-secondary" 128 | 129 | 130 | class VerticalDarkMenu(BasicMenu): 131 | navbar_classes = "navbar-vertical navbar-expand-lg navbar-dark" 132 | 133 | 134 | class LightMenu(BasicMenu): 135 | navbar_classes = "navbar-expand-lg navbar-light" 136 | 137 | 138 | class MenuMixin: 139 | menu_class = None 140 | menu_item = None 141 | submenu_class = None 142 | submenu_item = None 143 | 144 | def get_menu_class(self): 145 | return self.menu_class 146 | 147 | def get_menu_item(self): 148 | return self.menu_item 149 | 150 | def get_menu(self): 151 | menu_class = self.get_menu_class() 152 | if menu_class: 153 | menu_item = self.get_menu_item() 154 | return menu_class(menu_item) # pylint: disable=not-callable 155 | return None 156 | 157 | def get_submenu_class(self): 158 | return self.submenu_class 159 | 160 | def get_submenu_item(self): 161 | return self.submenu_item 162 | 163 | def get_submenu(self): 164 | submenu_class = self.get_submenu_class() 165 | if submenu_class: 166 | submenu_item = self.get_submenu_item() 167 | return submenu_class(submenu_item) # pylint: disable=not-callable 168 | return None 169 | 170 | def get_context_data(self, **kwargs): 171 | menu = self.get_menu() 172 | submenu = self.get_submenu() 173 | if menu: 174 | kwargs['menu'] = menu 175 | if submenu: 176 | kwargs['submenu'] = submenu 177 | return super().get_context_data(**kwargs) 178 | -------------------------------------------------------------------------------- /wildewidgets/settings.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/a1065ef39de1605d7304d4208d17404683beab21/wildewidgets/settings.py -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /wildewidgets/static/wildewidgets/css/_widget-list.scss: -------------------------------------------------------------------------------- 1 | .widget-list { 2 | &__sidebar { 3 | font-size: 1.2rem; 4 | } 5 | } 6 | 7 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/static/wildewidgets/images/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/a1065ef39de1605d7304d4208d17404683beab21/wildewidgets/static/wildewidgets/images/placeholder.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/altairchart.html: -------------------------------------------------------------------------------- 1 | {% if options.title %} 2 |
3 |

{{ options.title }}

4 |
5 | {% endif %} 6 |
7 |
8 |
9 | 24 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/apex_chart.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | -------------------------------------------------------------------------------- /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/barchart.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 97 | 98 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/templates/wildewidgets/categorychart.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 157 | 158 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/doughnutchart.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 57 | 58 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/doughnutchart_json.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 49 | 50 | -------------------------------------------------------------------------------- /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/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 |
-------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/html_widget.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{html|safe}} 4 |
-------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/initials_avatar.html: -------------------------------------------------------------------------------- 1 | 2 | {{fullname}} 3 | {{fullname}}{{initials}} 4 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/link_button.html: -------------------------------------------------------------------------------- 1 |
2 | {{ text }} 3 |
4 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/list_model_widget.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/a1065ef39de1605d7304d4208d17404683beab21/wildewidgets/templates/wildewidgets/list_model_widget.html -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/markdown_widget.html: -------------------------------------------------------------------------------- 1 | {% load markdownify %} 2 | 3 |
4 | {{text|markdownify}} 5 |
-------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/menu.html: -------------------------------------------------------------------------------- 1 | 57 | -------------------------------------------------------------------------------- /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 %} -------------------------------------------------------------------------------- /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 |
  • No {{item_label}}s
10 | {% endfor %} 11 | 12 | {% if is_paginated %} 13 |
    14 |
  • 15 | First 16 |
  • 17 | {% if page_obj.has_previous %} 18 |
  • 19 | Previous 20 |
  • 21 | {% else %} 22 |
  • 23 | Previous 24 |
  • 25 | {% endif %} 26 | {% for i in page_range %} 27 |
  • 28 | 29 | {{i}} 30 | 31 |
  • 32 | {% endfor %} 33 | 34 | {% if page_obj.has_next %} 35 |
  • 36 | Next 37 |
  • 38 | {% else %} 39 |
  • 40 | Previous 41 |
  • 42 | {% endif %} 43 |
  • 44 | Last 45 |
  • 46 |
47 | {% endif %} 48 | 49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/stackedbarchart.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 83 | 84 | -------------------------------------------------------------------------------- /wildewidgets/templates/wildewidgets/stackedbarchart_json.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 77 | 78 | -------------------------------------------------------------------------------- /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 %} -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caltechads/django-wildewidgets/a1065ef39de1605d7304d4208d17404683beab21/wildewidgets/templatetags/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /wildewidgets/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /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/views/json.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import os 3 | 4 | from django.apps import apps 5 | from django.http import JsonResponse 6 | from django.views.generic import View 7 | from django.views.generic.base import TemplateView 8 | 9 | from .mixins import WidgetInitKwargsMixin, JSONResponseMixin 10 | 11 | 12 | class JSONResponseView(JSONResponseMixin, TemplateView): 13 | pass 14 | 15 | 16 | class JSONDataView(View): 17 | 18 | def get(self, request, *args, **kwargs): 19 | context = self.get_context_data() 20 | return self.render_to_response(context) 21 | 22 | def get_context_data(self, **kwargs): 23 | return {} 24 | 25 | def render_to_response(self, context, **response_kwargs): 26 | return JsonResponse(context) 27 | 28 | 29 | class WildewidgetDispatch(WidgetInitKwargsMixin, View): 30 | 31 | def dispatch(self, request, *args, **kwargs): 32 | wildewidgetclass = request.GET.get('wildewidgetclass', None) 33 | csrf_token = request.GET.get('csrf_token', '') 34 | if wildewidgetclass: 35 | configs = apps.get_app_configs() 36 | for config in configs: 37 | check_file = os.path.join(config.path, "wildewidgets.py") 38 | check_dir = os.path.join(config.path, "wildewidgets") 39 | if os.path.isfile(check_file) or os.path.isdir(check_dir): 40 | module = importlib.import_module(f"{config.name}.wildewidgets") 41 | if hasattr(module, wildewidgetclass): 42 | class_ = getattr(module, wildewidgetclass) 43 | extra_data = self.get_decoded_extra_data(request) 44 | initargs = extra_data.get('args', []) 45 | initkwargs = extra_data.get('kwargs', {}) 46 | instance = class_(*initargs, **initkwargs) 47 | instance.request = request 48 | instance.csrf_token = csrf_token 49 | instance.args = initargs 50 | instance.kwargs = initkwargs 51 | return instance.dispatch(request, *args, **kwargs) 52 | -------------------------------------------------------------------------------- /wildewidgets/views/mixins.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional, TYPE_CHECKING 2 | import base64 3 | import json 4 | 5 | from django.core.serializers.json import DjangoJSONEncoder 6 | from django.http import HttpResponse 7 | from django.utils.cache import add_never_cache_headers 8 | try: 9 | from django.utils.encoding import force_text 10 | except ImportError: 11 | from django.utils.encoding import force_str as force_text # type: ignore 12 | from django.utils.functional import Promise 13 | 14 | if TYPE_CHECKING: 15 | from ..widgets import BreadcrumbBlock 16 | 17 | 18 | class LazyEncoder(DjangoJSONEncoder): 19 | """ 20 | Encodes django's lazy i18n strings. 21 | """ 22 | def default(self, o): 23 | if isinstance(o, Promise): 24 | return force_text(o) 25 | return super(LazyEncoder, self).default(o) 26 | 27 | 28 | class WidgetInitKwargsMixin: 29 | 30 | def __init__(self, *args, **kwargs): 31 | super().__init__(*args, **kwargs) 32 | self.extra_data = { 33 | "args": args, 34 | "kwargs": kwargs 35 | } 36 | 37 | def get_encoded_extra_data(self): 38 | data_json = json.dumps(self.extra_data) 39 | payload_bytes = base64.b64encode(data_json.encode()) 40 | payload = payload_bytes.decode() 41 | return payload 42 | 43 | def get_decoded_extra_data(self, request): 44 | encoded_extra_data = request.GET.get("extra_data", None) 45 | if not encoded_extra_data: 46 | return {} 47 | extra_bytes = encoded_extra_data.encode() 48 | payload_bytes = base64.b64decode(extra_bytes) 49 | payload = json.loads(payload_bytes.decode()) 50 | return payload 51 | 52 | def convert_extra(self, extra_item, first=True): 53 | if first: 54 | start = '?' 55 | else: 56 | start = '&' 57 | if isinstance(extra_item, dict): 58 | extra_list = [] 59 | for k, v in extra_item.items(): 60 | extra_list.append(f"{k}={v}") 61 | extra = f"{start}{'&'.join(extra_list)}" 62 | return extra 63 | return '' 64 | 65 | 66 | class JSONResponseMixin: 67 | 68 | is_clean: bool = False 69 | 70 | def render_to_response(self, context: Dict[str, Any]) -> str: 71 | """ 72 | Returns a JSON response containing 'context' as payload 73 | """ 74 | return self.get_json_response(context) 75 | 76 | def get_json_response(self, content: Any, **httpresponse_kwargs) -> HttpResponse: 77 | """ 78 | Construct an `HttpResponse` object. 79 | """ 80 | response = HttpResponse( 81 | content, 82 | content_type='application/json', 83 | **httpresponse_kwargs 84 | ) 85 | add_never_cache_headers(response) 86 | return response 87 | 88 | def post(self, *args, **kwargs): 89 | return self.get(*args, **kwargs) 90 | 91 | def get(self, request, *args, **kwargs): 92 | self.request = request 93 | self.csrf_token = self.request.GET.get('csrf_token', None) 94 | response = None 95 | 96 | func_val = self.get_context_data(**kwargs) 97 | if not self.is_clean: 98 | assert isinstance(func_val, dict) 99 | response = dict(func_val) 100 | if 'error' not in response and 'sError' not in response: 101 | response['result'] = 'ok' 102 | else: 103 | response['result'] = 'error' 104 | else: 105 | response = func_val 106 | # can't have 'view' here, because the view object can't be jsonified 107 | response.pop('view', None) 108 | 109 | dump = json.dumps(response, cls=LazyEncoder) 110 | return self.render_to_response(dump) 111 | 112 | 113 | class StandardWidgetMixin: 114 | """ 115 | A class based view mixin for views that use a standard widget template. 116 | This is used with a template-less design. 117 | 118 | The template used by your derived class should include at least the following:: 119 | 120 | {% extends ".html" %} 121 | {% load wildewidgets %} 122 | 123 | {% block title %}{{page_title}}{% endblock %} 124 | 125 | {% block breadcrumb-items %} 126 | {% if breadcrumbs %} 127 | {% wildewidgets breadcrumbs %} 128 | {% endif %} 129 | {% endblock %} 130 | 131 | {% block content %} 132 | {% wildewidgets content %} 133 | {% endblock %} 134 | 135 | The ``content`` block is where the content of the page is rendered. The 136 | ``breadcrumbs`` block is where the breadcrumbs are rendered. 137 | 138 | An example derived class, which will be used by most of the views in the project:: 139 | 140 | class DemoStandardMixin(StandardWidgetMixin, NavbarMixin): 141 | template_name='core/standard.html' 142 | menu_class = DemoMenu 143 | 144 | The ``DemoStandardMixin`` class is used in the following way:: 145 | 146 | class HomeView(DemoStandardMixin, TemplateView): 147 | menu_item = 'Home' 148 | 149 | def get_content(self): 150 | return HomeBlock() 151 | 152 | def get_breadcrumbs(self): 153 | breadcrumbs = DemoBaseBreadcrumbs() 154 | breadcrumbs.add_breadcrumb('Home') 155 | return breadcrumbs 156 | """ 157 | 158 | def get_context_data(self, **kwargs): 159 | kwargs['content'] = self.get_content() 160 | breadcrumbs: "Optional[BreadcrumbBlock]" = self.get_breadcrumbs() 161 | if breadcrumbs: 162 | kwargs['breadcrumbs'] = breadcrumbs 163 | kwargs['page_title'] = breadcrumbs.flatten() 164 | return super().get_context_data(**kwargs) 165 | 166 | def get_content(self): 167 | raise NotImplementedError( 168 | f"You must override get_content in {self.__class__.__name__}" 169 | ) 170 | 171 | def get_breadcrumbs(self) -> "Optional[BreadcrumbBlock]": 172 | return None 173 | -------------------------------------------------------------------------------- /wildewidgets/views/tables.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Type, TYPE_CHECKING 2 | 3 | from django.core.exceptions import ImproperlyConfigured 4 | from django.http import HttpResponseRedirect 5 | from django.views.generic import View 6 | from django.views.generic.base import TemplateView 7 | 8 | if TYPE_CHECKING: 9 | from ..widgets import BaseDataTable 10 | 11 | 12 | class TableActionFormView(View): 13 | """ 14 | A view that handles a form action on a table. You just need to define the :py:meth:`process_form_action` method. 15 | """ 16 | 17 | http_method_names: List[str] = ['post'] 18 | url: Optional[str] = None 19 | 20 | def process_form_action(self, action: str, items: List[str]) -> None: 21 | method_name = f'process_{action}_action' 22 | if hasattr(self, method_name): 23 | getattr(self, method_name)(items) 24 | 25 | def post(self, request, *args, **kwargs): 26 | checkboxes = request.POST.getlist('checkbox') 27 | action = request.POST.get('action') 28 | self.process_form_action(action, checkboxes) 29 | return HttpResponseRedirect(self.url) 30 | 31 | 32 | class TableView(TemplateView): 33 | 34 | table_class: "Optional[Type[BaseDataTable]]" = None 35 | 36 | def get_context_data(self, **kwargs): 37 | if not self.table_class: 38 | raise ImproperlyConfigured( 39 | f"You must set a table_class attribute on {self.__class__.__name__}" 40 | ) 41 | kwargs['table'] = self.table_class() # pylint: disable=not-callable 42 | return super().get_context_data(**kwargs) 43 | -------------------------------------------------------------------------------- /wildewidgets/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from .base import * # noqa: F403,F401 5 | from .buttons import * # noqa: F403,F401 6 | from .charts import * # noqa: F403,F401 7 | from .datagrid import * # noqa: F403,F401 8 | from .forms import * # noqa: F403,F401 9 | from .grid import * # noqa: F403,F401 10 | from .headers import * # noqa: F403,F401 11 | from .icons import * # noqa: F403,F401 12 | from .layout import * # noqa: F403,F401 13 | from .misc import * # noqa: F403,F401 14 | from .modals import * # noqa: F403,F401 15 | from .navigation import * # noqa: F403,F401 16 | from .structure import * # noqa: F403,F401 17 | from .tables import * # noqa: F403,F401 18 | from .text import * # noqa: F403,F401 19 | -------------------------------------------------------------------------------- /wildewidgets/widgets/charts/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from .altair import * # noqa: F403,F401 5 | from .apex import * # noqa: F403,F401 6 | from .chartjs import * # noqa: F403,F401 7 | 8 | -------------------------------------------------------------------------------- /wildewidgets/widgets/charts/altair.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import random 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 | template_file = 'wildewidgets/altairchart.html' 15 | title = None 16 | width = "100%" 17 | height = "300px" 18 | 19 | def __init__(self, *args, **kwargs): 20 | self.data = None 21 | self.options = { 22 | 'width': kwargs.get('width', self.width), 23 | 'height': kwargs.get('height', self.height), 24 | "title": kwargs.get('title', self.title) 25 | } 26 | 27 | def get_content(self, **kwargs): 28 | chart_id = random.randrange(0, 1000) 29 | template_file = self.template_file 30 | if self.data: 31 | context = self.get_context_data() 32 | else: 33 | context = {"async": True} 34 | html_template = template.loader.get_template(template_file) 35 | context['options'] = self.options 36 | context['name'] = f"altair_chart_{chart_id}" 37 | context["wildewidgetclass"] = self.__class__.__name__ 38 | content = html_template.render(context) 39 | return content 40 | 41 | def __str__(self): 42 | return self.get_content() 43 | 44 | def get_context_data(self, **kwargs): 45 | context = super().get_context_data(**kwargs) 46 | self.load() 47 | context.update({"data": self.data}) 48 | return context 49 | 50 | def set_data(self, spec, set_size=True): 51 | if set_size: 52 | self.data = spec.properties( 53 | width="container", 54 | height="container" 55 | ).to_dict() 56 | else: 57 | self.data = spec.to_dict() 58 | 59 | def load(self): 60 | pass 61 | -------------------------------------------------------------------------------- /wildewidgets/widgets/charts/apex.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import json 6 | import random 7 | from typing import Optional 8 | 9 | from django import template 10 | from django.http import JsonResponse 11 | 12 | from wildewidgets.views import WidgetInitKwargsMixin 13 | 14 | from ..base import Widget 15 | 16 | 17 | class ApexDatasetBase(Widget): 18 | name: Optional[str] = None 19 | chart_type: Optional[str] = None 20 | 21 | def __init__(self, **kwargs): 22 | self.data = [] 23 | 24 | def get_options(self): 25 | options = { 26 | 'data': self.data 27 | } 28 | if self.name: 29 | options['name'] = self.name if self.name is not None else "" 30 | if self.chart_type: 31 | options['type'] = self.chart_type 32 | return options 33 | 34 | 35 | class ApexJSONMixin: 36 | """ 37 | A mixin class adding AJAX support to Apex Charts 38 | """ 39 | template_name: str = 'wildewidgets/apex_json.html' 40 | 41 | def dispatch(self, request, *args, **kwargs): 42 | handler = getattr(self, request.method.lower()) 43 | return handler(request, *args, **kwargs) 44 | 45 | def get(self, request, *args, **kwargs): 46 | data = self.get_series_data() 47 | return self.render_to_response(data) 48 | 49 | def render_to_response(self, context, **response_kwargs): 50 | return JsonResponse(context) 51 | 52 | def get_series_data(self, **kwargs): 53 | self.load() 54 | kwargs['series'] = self.chart_options['series'] 55 | return kwargs 56 | 57 | def load(self): 58 | """ 59 | Load datasets into the chart via AJAX. 60 | """ 61 | pass 62 | 63 | 64 | class ApexChartBase(WidgetInitKwargsMixin): 65 | template_name = 'wildewidgets/apex_chart.html' 66 | css_id = None 67 | chart_type = None 68 | 69 | def __init__(self, *args, **kwargs): 70 | super().__init__(*args, **kwargs) 71 | self.chart_options = {} 72 | self.chart_options['series'] = [] 73 | self.css_id = kwargs.get('css_id', self.css_id) 74 | self.chart_options['chart'] = {} 75 | if self.chart_type: 76 | self.chart_options['chart']['type'] = self.chart_type 77 | 78 | def add_dataset(self, dataset): 79 | self.chart_options['series'].append(dataset.get_options()) 80 | 81 | def add_categories(self, categories): 82 | if 'xaxis' not in self.chart_options: 83 | self.chart_options['xaxis'] = {} 84 | self.chart_options['xaxis']['categories'] = categories 85 | 86 | def get_context_data(self, **kwargs): 87 | # kwargs = super().get_context_data(**kwargs) 88 | kwargs['options'] = json.dumps(self.chart_options) 89 | if not self.css_id: 90 | self.css_id = random.randint(1, 100000) 91 | kwargs['css_id'] = self.css_id 92 | kwargs["wildewidgetclass"] = self.__class__.__name__ 93 | kwargs["extra_data"] = self.get_encoded_extra_data() 94 | return kwargs 95 | 96 | def add_suboption(self, option, name, value): 97 | if option not in self.chart_options: 98 | self.chart_options[option] = {} 99 | self.chart_options[option][name] = value 100 | 101 | def __str__(self): 102 | return self.get_content() 103 | 104 | def get_content(self, **kwargs): 105 | context = self.get_context_data() 106 | html_template = template.loader.get_template(self.template_name) 107 | content = html_template.render(context) 108 | return content 109 | 110 | 111 | class ApexSparkline(ApexChartBase): 112 | width = 65 113 | stroke_width = 2 114 | 115 | def __init__(self, **kwargs): 116 | super().__init__(**kwargs) 117 | self.add_suboption('chart', 'sparkline', {'enabled': True}) 118 | self.add_suboption('chart', 'width', self.width) 119 | self.add_suboption('stroke', 'width', self.stroke_width) 120 | -------------------------------------------------------------------------------- /wildewidgets/widgets/datagrid.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from typing import Any, Dict, List, Optional, Tuple, Union, cast 3 | 4 | from django.core.exceptions import ImproperlyConfigured 5 | 6 | from .base import Block 7 | 8 | 9 | DatagridItemDef = Union["DatagridItem", Tuple[str, str], Tuple[str, str, Dict[str, Any]]] 10 | 11 | 12 | class DatagridItem(Block): 13 | """ 14 | This widget implements a `Tabler datagrid-item 15 | `_ It should be used with 16 | :py:class:`Datagrid`. 17 | 18 | Note: 19 | Unlike :py:class:`wildewidgets.widgets.base.Block`, :py:class:`DatagridItem` requires 20 | either :py:attr:`contents` to be set, or the block contents to be 21 | provided as positional arguments. 22 | 23 | Keyword Args: 24 | title: the ``datagrid-title`` of the ``datagrid-item`` 25 | url: URL to use to turn content into a hyperlink 26 | 27 | Raises: 28 | ValueError: either the ``title`` was empty, or no contents were provided 29 | ImproperlyConfigured: :py:attr:`url` was set, and there is more than one block 30 | in :py:attr:`contents` 31 | """ 32 | block: str = 'datagrid-item' 33 | 34 | #: the ``datagrid-title`` of the ``datagrid-item`` 35 | title: Optional[str] = None 36 | #: a URL to use to turn our contents into a hyperlink 37 | url: Optional[str] = None 38 | 39 | def __init__( 40 | self, 41 | *blocks, 42 | title: str = None, 43 | url: str = None, 44 | **kwargs 45 | ) -> None: 46 | self.title = title if title else self.title 47 | if not self.title: 48 | raise ValueError('"title" is required as either a keyword argument or as a class attribute') 49 | self.url = url if url else self.url 50 | super().__init__(*blocks, **kwargs) 51 | 52 | def add_blocks(self) -> None: 53 | """ 54 | Add our content. 55 | 56 | If :py:attr:`url` is set, and there is only one block in :py:attr:`contents`, wrap 57 | that block in a :py:class:`wildewidgets.Link`. 58 | 59 | Raises: 60 | ImproperlyConfigured: :py:attr:`url` was set, and there is more than one block 61 | in :py:attr:`contents` 62 | """ 63 | if self.url: 64 | if len(self.contents) == 1: 65 | wrapper: Block = Block( 66 | self.contents[0], 67 | tag='a', 68 | attributes={'href': self.url}, 69 | name='datagrid-content' 70 | ) 71 | else: 72 | raise ImproperlyConfigured( 73 | f'{self.__class__.__name__}: url should only be set if contents are a single block' 74 | ) 75 | else: 76 | if len(self.contents) == 1: 77 | if not isinstance(self.contents[0], Block): 78 | # Wrap the text in a block to allow us to assign the datagrid-content 79 | # class to it 80 | wrapper = Block(self.contents[0], name='datagrid-content') 81 | else: 82 | wrapper = self.contents[0] 83 | else: 84 | wrapper = Block(name='datagrid-content') 85 | for block in self.contents: 86 | wrapper.add_block(block) 87 | self.add_block(Block(self.title, name='datagrid-title')) 88 | self.add_block(wrapper) 89 | 90 | 91 | class Datagrid(Block): 92 | """ 93 | This widget implements a `Tabler Data grid `_ 94 | It contains :py:class:`DatagridItem` objects. 95 | 96 | Examples: 97 | 98 | Add :py:class:`DatagridItem` objects to this in one of these ways: 99 | 100 | As constructor arguments:: 101 | 102 | >>> item1 = DatagridItem(title='foo', content='bar', url="https://example.com") 103 | >>> item2 = DatagridItem(title='baz', content='barney') 104 | >>> item3 = ['foo', 'bar'] 105 | >>> grid = Datagrid(item1, item2, item3) 106 | 107 | By using :py:meth:`add_block` with a :py:class:`DatagridItem`: 108 | 109 | >>> grid = Datagrid(item1, item2, item3) 110 | >>> grid.add_block(DatagridItem(title='foo', content='bar')) 111 | 112 | By using :py:meth:`add_item`:: 113 | 114 | >>> grid = Datagrid(item1, item2, item3) 115 | >>> grid.add_item('foo', 'bar') 116 | 117 | Args: 118 | *items: a list of ``datagrid-item`` definitions or :py:class:`DatagridItem` objects. 119 | """ 120 | block: str = 'datagrid' 121 | #: a list of ``datagrid-items`` to add to our content 122 | items: List[DatagridItemDef] = [] 123 | 124 | def __init__(self, *items: DatagridItemDef, **kwargs): 125 | self.items = list(items) if items else deepcopy(self.items) 126 | super().__init__(**kwargs) 127 | for item in items: 128 | if isinstance(item, DatagridItem): 129 | self.add_block(item) 130 | elif isinstance(item, tuple): 131 | if len(item) == 2: 132 | item = cast(Tuple[str, str], item) 133 | self.add_item(item[0], item[1]) 134 | else: 135 | item = cast(Tuple[str, str, Dict[str, Any]], item) 136 | self.add_item(item[0], item[1], **item[2]) 137 | 138 | def add_item( 139 | self, 140 | title: str, 141 | content: Union[Union[str, Block], List[Union[str, Block]]], 142 | url: str = None, 143 | **kwargs 144 | ) -> None: 145 | """ 146 | Add a :py:class:`DatagridItem` to our block contents, with 147 | ``datagrid-title`` of ``title`` and datagrid. 148 | 149 | Examples: 150 | 151 | Start with our grid:: 152 | 153 | >>> dg = DataGrid() 154 | 155 | Add a simple key/value:: 156 | 157 | >>> dg.add_item('Version', '1.2.3') 158 | 159 | Add a block as the value, and wrap it in a :py:class:`wildewdigets.Link`:: 160 | 161 | >>> dg.add_item( 162 | 'Gravatar', 163 | Image(src=static('myapp/images/gravatar.png'), alt='MyGravatar'), 164 | url='https://www.google.com' 165 | ) 166 | 167 | Add a list of blocks as the value:: 168 | 169 | >>> dg.add_item( 170 | 'Contributors', 171 | [ 172 | ImageLink( 173 | src=static('myapp/images/fred-gravatar.png', 174 | alt='Fred' 175 | url='https://www.fred.com' 176 | ), 177 | ImageLink( 178 | src=static('myapp/images/barney-gravatar.png', 179 | alt='Barney' 180 | url='https://www.barney.com' 181 | ) 182 | ], 183 | css_class='d-flex flex-row' 184 | ) 185 | 186 | 187 | Note: 188 | To add a :py:class:`DatagridItem` directly, use :py:meth:`add_block`. 189 | 190 | Keyword Args: 191 | title: the ``datagrid-title`` of the ``datagrid-item`` 192 | content: the ``datagrid-content`` of the ``datagrid-item`` 193 | url: URL to use to turn content into a hyperlink 194 | """ 195 | if isinstance(content, str) or isinstance(content, Block): 196 | content = [content] 197 | self.add_block(DatagridItem(*content, title=title, url=url, **kwargs)) 198 | -------------------------------------------------------------------------------- /wildewidgets/widgets/icons.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from .base import Block 4 | 5 | 6 | class FontIcon(Block): 7 | """ 8 | Render a font-based Bootstrap icon, for example:: 9 | 10 | 11 | 12 | See the `Boostrap Icons `_ list for the 13 | list of icons. Find an icon you like, and use the name of that icon on that 14 | page as the ``icon`` kwarg to the constructor, or set it as the :py:attr:`icon` 15 | class variable. 16 | 17 | Keyword Args: 18 | icon: the name of the icon to render, from the Bootstrap Icons list 19 | color: use this as Tabler color name to use as the foreground 20 | font color, leaving the background transparent. If ``background`` 21 | is also set, this is ignored. Look at `Tabler: Colors 22 | `_ 23 | for your choices; set this to the text after the ``bg-`` 24 | background: use this as Tabler background/foreground color set for 25 | this icon. : This overrides :py:attr:`color`. Look 26 | at `Tabler: Colors `_ 27 | for your choices; set this to the text after the ``bg-`` 28 | """ 29 | 30 | tag: str = 'i' 31 | block: str = 'fonticon' 32 | 33 | #: The icon font family prefix. One could override this to use FontAwesome icons, 34 | #: for instance, buy changing it to ``fa`` 35 | prefix: str = 'bi' 36 | 37 | #: Use this as the name for the icon to render 38 | icon: Optional[str] = None 39 | #: If not ``None``, use this as Tabler color name to use as the foreground 40 | #: font color, leaving the background transparent. If :py:attr:`background` 41 | #: is also set, this is ignored. Look at `Tabler: Colors 42 | # `_ for your choices; set 43 | #: this to the text after the ``bg-`` 44 | color: Optional[str] = None 45 | #: If not ``None``, use this as Tabler background/foreground color set for 46 | #: this icon. : This overrides :py:attr:`color`. Look 47 | #: at `Tabler: Colors `_ 48 | #: for your choices; set this to the text after the ``bg-`` 49 | background: Optional[str] = None 50 | 51 | def __init__( 52 | self, 53 | icon: Optional[str] = None, 54 | color: Optional[str] = None, 55 | background: Optional[str] = None, 56 | **kwargs 57 | ) -> None: 58 | super().__init__(**kwargs) 59 | self.color = color if color else self.color 60 | self.background = background if background else self.background 61 | self.icon = icon if icon else self.icon 62 | if not self.icon: 63 | raise ValueError('"icon" is required as either a keyword argument or as a class attribute') 64 | self.icon = f'{self.prefix}-{icon}' 65 | self.add_class(self.icon) 66 | if self.color: 67 | self.add_class(f'text-{self.color} bg-transparent') 68 | elif self.background: 69 | self.add_class(f' bg-{self.background} text-{self.background}-fg') 70 | 71 | 72 | class TablerFontIcon(FontIcon): 73 | """ 74 | :py:class:`FontIcon` for Tabler Icons. 75 | 76 | Requires:: 77 | 78 | 79 | """ 80 | prefix = 'ti ti' 81 | 82 | 83 | class TablerMenuIcon(FontIcon): 84 | """ 85 | A Tabler menu specific icon. This just adds some menu specific classes and 86 | uses a ```` instead of a ````. It is used by 87 | :py:class:`wildewidgets.NavItem`, :py:class:`wildewidgets.NavDropdownItem` 88 | and :py:class:`wildewidgets.DropdownItem` objects. 89 | 90 | Typically, you won't use this directly, but instead it will be created for 91 | you from a :py:class:`wildewdigets.MenuItem` specification when 92 | :py:class:`wildewdigets.MenuItem.icon` is not ``None``. 93 | 94 | Example: 95 | 96 | >>> icon = TablerMenuIcon(icon='target') 97 | >>> item = NavItem(text='Page', url='/page', icon=icon) 98 | >>> item2 = DropdownItem(text='Page', url='/page', icon=icon) 99 | >>> item3 = NavDropdownItem(text='Page', url='/page', icon=icon) 100 | """ 101 | 102 | tag: str = 'span' 103 | block: str = 'nav-link-icon' 104 | css_class: str = 'd-md-none d-lg-inline-block' 105 | -------------------------------------------------------------------------------- /wildewidgets/widgets/misc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from typing import Dict, Any, Optional, Union 4 | 5 | from .base import Block, TemplateWidget, Image 6 | from .structure import HorizontalLayoutBlock 7 | from .text import CodeWidget 8 | 9 | 10 | class KeyValueListBlock(Block): 11 | 12 | tag: str = 'ul' 13 | block: str = "list-group" 14 | 15 | def add_simple_key_value(self, key, value): 16 | self.add_block( 17 | HorizontalLayoutBlock( 18 | Block(key), 19 | Block(value), 20 | tag="li", 21 | css_class="list-group-item", 22 | ) 23 | ) 24 | 25 | def add_code_key_value(self, key, value, language=None): 26 | self.add_block( 27 | Block( 28 | Block(key), 29 | CodeWidget( 30 | code=value, 31 | language=language, 32 | css_class="m-3", 33 | ), 34 | tag="li", 35 | css_class="list-group-item", 36 | ) 37 | ) 38 | 39 | 40 | class GravatarWidget(Image): 41 | 42 | block: str = 'rounded-circle' 43 | 44 | #: The gravatar URL 45 | gravatar_url: Optional[str] = None 46 | #: The length in pixels that will used as the height and width of the image 47 | size: Union[int, str] = 28 48 | #: The person's name. This will be used as the ``alt`` tag on the image 49 | fullname: Optional[str] = None 50 | 51 | def __init__( 52 | self, 53 | gravatar_url: str = None, 54 | size: Union[int, str] = None, 55 | fullname: str = None, 56 | **kwargs 57 | ): 58 | self.gravatar_url = gravatar_url if gravatar_url is not None else self.gravatar_url 59 | self.size = size if size is not None else self.size 60 | self.fullname = fullname if fullname is not None else self.fullname 61 | try: 62 | int(self.size) 63 | except ValueError as e: 64 | raise ValueError(f'size should be an integer; got "{self.size}" instead') from e 65 | kwargs['src'] = self.gravatar_url 66 | if self.fullname: 67 | kwargs['alt'] = self.fullname 68 | super().__init__(**kwargs) 69 | self._attributes['style'] = f'width: {self.size}px; height: {self.size}px' 70 | 71 | 72 | class InitialsAvatarWidget(TemplateWidget): 73 | 74 | template_name: str = 'wildewidgets/initials_avatar.html' 75 | 76 | #: The length in pixels that will used as the height and width of the gravatar 77 | size: int = 28 78 | #: The foreground color for the gravatar 79 | color: str = "white" 80 | #: The background color for the gravatar 81 | background_color: str = "#626976" 82 | 83 | def __init__( 84 | self, 85 | *args, 86 | initials: str = None, 87 | size: int = None, 88 | color: str = None, 89 | background_color: str = None, 90 | fullname: str = None, 91 | **kwargs 92 | ): 93 | self.initials = initials 94 | self.size = size or self.size 95 | self.color = color or self.color 96 | self.background_color = background_color or self.background_color 97 | self.fullname = fullname 98 | super().__init__(*args, **kwargs) 99 | 100 | def get_context_data(self, **kwargs) -> Dict[str, Any]: 101 | kwargs = super().get_context_data(**kwargs) 102 | kwargs['initials'] = self.initials.upper() 103 | kwargs['fullsize'] = str(self.size) 104 | kwargs['halfsize'] = str(self.size / 2) 105 | kwargs['color'] = self.color 106 | kwargs['background_color'] = self.background_color 107 | kwargs['fullname'] = self.fullname 108 | return kwargs 109 | -------------------------------------------------------------------------------- /wildewidgets/widgets/modals.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from django.core.exceptions import ImproperlyConfigured 5 | 6 | 7 | from .base import Block 8 | from .forms import CrispyFormWidget 9 | 10 | 11 | class ModalWidget(Block): 12 | """ 13 | Renders a Bootstrap 5 Modal. 14 | 15 | Args: 16 | modal_id: The CSS ID of the modal. 17 | modal_title: The title of the modal. 18 | modal_body: The body of the modal, any Block. 19 | modal_size (optional): The size of the modal. One of 'sm', 'lg', or 'xl'. 20 | """ 21 | 22 | template_name = "wildewidgets/modal.html" 23 | modal_id = None 24 | modal_title = None 25 | modal_body = None 26 | modal_size = None 27 | 28 | def __init__( 29 | self, 30 | modal_id=None, 31 | modal_title=None, 32 | modal_body=None, 33 | modal_size=None, 34 | **kwargs, 35 | ): 36 | self.modal_id = modal_id if modal_id else self.modal_id 37 | self.modal_title = modal_title if modal_title else self.modal_title 38 | self.modal_body = modal_body if modal_body else self.modal_body 39 | self.modal_size = modal_size if modal_size else self.modal_size 40 | super().__init__(**kwargs) 41 | 42 | def get_context_data(self, *args, **kwargs): 43 | kwargs = super().get_context_data(*args, **kwargs) 44 | kwargs["modal_id"] = self.modal_id 45 | kwargs["modal_title"] = self.modal_title 46 | kwargs["modal_body"] = self.modal_body 47 | kwargs["modal_size"] = self.modal_size 48 | return kwargs 49 | 50 | 51 | class CrispyFormModalWidget(ModalWidget): 52 | """ 53 | Renders a Bootstrap 5 Modal with a CrispyFormWidget as the body. 54 | 55 | Args: 56 | form: The form to render in the modal. 57 | form_class: The form class to render in the modal. 58 | """ 59 | 60 | form_class = None 61 | form = None 62 | 63 | def __init__(self, form=None, form_class=None, **kwargs): 64 | if not form: 65 | form = self.form 66 | if not form_class: 67 | form_class = self.form_class 68 | 69 | if form_class: 70 | modal_form = form_class() 71 | elif form: 72 | modal_form = form 73 | else: 74 | raise ImproperlyConfigured("Either 'form_class' or 'form' must be set") 75 | modal_body = CrispyFormWidget(form=modal_form) 76 | kwargs["modal_body"] = modal_body 77 | super().__init__(**kwargs) 78 | -------------------------------------------------------------------------------- /wildewidgets/widgets/tables/__init__.py: -------------------------------------------------------------------------------- 1 | from .actions import * # noqa: F403,F401 2 | from .components import * # noqa: F403,F401 3 | from .tables import * # noqa: F403,F401 4 | from .views import * # noqa: F403,F401 5 | -------------------------------------------------------------------------------- /wildewidgets/widgets/tables/components.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | 3 | 4 | class DataTableColumn: 5 | 6 | def __init__( 7 | self, 8 | field: str, 9 | verbose_name: str = None, 10 | searchable: bool = False, 11 | sortable: bool = False, 12 | align: str = 'left', 13 | head_align: str = 'left', 14 | visible: bool = True, 15 | wrap: bool = True 16 | ): 17 | self.field = field 18 | self.verbose_name = verbose_name if verbose_name else self.field.capitalize() 19 | self.searchable = searchable 20 | self.sortable = sortable 21 | self.align = align 22 | self.head_align = head_align 23 | self.visible = visible 24 | self.wrap = wrap 25 | 26 | 27 | class DataTableFilter: 28 | 29 | def __init__(self, header=None): 30 | self.header = header 31 | self.choices: List[Tuple[str, str]] = [('Any', '')] 32 | 33 | def add_choice(self, label: str, value: str) -> None: 34 | self.choices.append((label, value)) 35 | 36 | 37 | class DataTableStyler: 38 | 39 | def __init__(self, is_row, test_cell, cell_value, css_class, target_cell=None): 40 | self.is_row = is_row 41 | self.test_cell = test_cell 42 | self.cell_value = cell_value 43 | self.css_class = css_class 44 | self.target_cell = target_cell 45 | self.test_index = 0 46 | self.target_index = 0 47 | 48 | 49 | class DataTableForm: 50 | 51 | def __init__(self, table): 52 | if table.has_form_actions(): 53 | self.is_visible: bool = True 54 | else: 55 | self.is_visible = False 56 | self.actions = table.get_form_actions() 57 | self.url = table.form_url 58 | -------------------------------------------------------------------------------- /wildewidgets/widgets/text.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import warnings 5 | from typing import Optional 6 | 7 | try: 8 | from pygments import highlight 9 | from pygments.lexers import get_lexer_by_name 10 | from pygments.formatters import HtmlFormatter 11 | except ModuleNotFoundError: 12 | # Only needed if using syntax highlighting 13 | pass 14 | 15 | from .base import TemplateWidget, Block 16 | 17 | 18 | class CodeWidget(Block): 19 | """ 20 | A widget to display code with syntax highlighting if a language is supplied. 21 | 22 | Keyword Args: 23 | code: the code to be displayed 24 | language: the language of the code 25 | line_numbers: if ``True``, show line numbers 26 | """ 27 | block: str = 'wildewidgets_highlight_container' 28 | 29 | #: The code to be displayed 30 | code: str = "" 31 | #: The programming language for the :py:attr:`code` 32 | language: Optional[str] = None 33 | #: If ``True``, show line numbers 34 | line_numbers: bool = False 35 | 36 | def __init__( 37 | self, 38 | code: str = None, 39 | language: str = None, 40 | line_numbers: bool = False, 41 | **kwargs 42 | ): 43 | self.code = code if code else self.code 44 | self.language = language if language else self.language 45 | if not self.language: 46 | raise ValueError( 47 | f'{self.__class__.__name__}: "language" must be defined either as a class attribute or a keyword arg' 48 | ) 49 | self.line_numbers = line_numbers if line_numbers else self.line_numbers 50 | super().__init__(**kwargs) 51 | self.add_code(self.code, language=self.language, line_numbers=self.line_numbers) 52 | 53 | def add_code(self, code: str, language: str, line_numbers: bool = False) -> None: 54 | lexer = get_lexer_by_name(language) 55 | formatter = HtmlFormatter(linenos=line_numbers, cssclass="wildewidgets_highlight") 56 | self.add_block(highlight(code, lexer, formatter)) 57 | 58 | 59 | class MarkdownWidget(TemplateWidget): 60 | """ 61 | A widget to display markdown as HTML. 62 | 63 | Args: 64 | text (str): the markdown to render as HTML. 65 | css_class (str, optional): any classes to add to the widget 66 | """ 67 | template_name = 'wildewidgets/markdown_widget.html' 68 | text: str = "" 69 | css_class: str = "" 70 | 71 | def __init__(self, *args, **kwargs): 72 | self.text = kwargs.pop("text", self.text) 73 | self.css_class = kwargs.pop("css_class", self.css_class) 74 | super().__init__(*args, **kwargs) 75 | 76 | def get_context_data(self, *args, **kwargs): 77 | kwargs = super().get_context_data(*args, **kwargs) 78 | kwargs['text'] = self.text 79 | kwargs['css_class'] = self.css_class 80 | return kwargs 81 | 82 | 83 | class HTMLWidget(TemplateWidget): 84 | """ 85 | A widget to display raw HTML. 86 | 87 | Args: 88 | html (str): the HTML to render. 89 | css_class (str, optional): any classes to add to the widget 90 | """ 91 | template_name = 'wildewidgets/html_widget.html' 92 | html = "" 93 | css_class = None 94 | 95 | def __init__(self, *args, **kwargs): 96 | self.html = kwargs.pop('html', self.html) 97 | self.css_class = kwargs.pop("css_class", self.css_class) 98 | super().__init__(*args, **kwargs) 99 | 100 | def get_context_data(self, *args, **kwargs): 101 | kwargs = super().get_context_data(*args, **kwargs) 102 | kwargs['html'] = self.html 103 | kwargs['css_class'] = self.css_class 104 | return kwargs 105 | 106 | 107 | class StringBlock(Block): 108 | """ 109 | A basic widget that displays a string. 110 | 111 | .. deprecated:: 0.14.0 112 | 113 | Use :py:class:`wildewidgets.widgets.base.Block` directly instead. It 114 | works exactly like :py:class:`StringBlock` 115 | 116 | Args: 117 | text: the text to display. 118 | """ 119 | 120 | def __init__(self, text: str, **kwargs): 121 | warnings.warn( 122 | 'Deprecated in 0.14.0; use Block directly instead.', 123 | DeprecationWarning, 124 | stacklevel=2 125 | ) 126 | super().__init__(*[text], **kwargs) 127 | 128 | 129 | class TimeStamp(Block): 130 | """ 131 | A basic widget that displays a timestamp. 132 | 133 | Args: 134 | text: the text to display. 135 | """ 136 | tag = "small" 137 | css_class = "fw-light" 138 | 139 | 140 | class TagBlock(Block): 141 | """ 142 | A basic widget that displays a colored tag. 143 | 144 | Args: 145 | text: the text to display. 146 | color: the bootstrap color class. 147 | """ 148 | block: str = 'badge' 149 | 150 | def __init__(self, text: str, color: str = "secondary", **kwargs): 151 | super().__init__(text, **kwargs) 152 | self.add_class(f'bg-{color}') 153 | --------------------------------------------------------------------------------