From 66e32b8dc5ea5e9685130c31988d04ce9a6862c6 Mon Sep 17 00:00:00 2001 From: Seva D Date: Sat, 10 Jun 2023 14:39:12 +0400 Subject: [PATCH] Added RUFF linter. Cleaned up code. Removed pydantic dependency. (#32) Added RUFF linter. Cleaned up code. Removed pydantic dependency. --- .flake8 | 5 - .github/workflows/action.yml | 3 +- Makefile | 100 +-- README.md | 34 +- docs/README.md | 687 ------------------ {tests/models/orms => docs}/__init__.py | 0 docs/build.py | 295 +++----- docs/index.html | 280 +++---- examples/__init__.py | 5 +- examples/dashboard/djangoorm.py | 20 +- examples/dashboard/tortoise.py | 14 +- examples/inlines/tortoise.py | 32 +- examples/models/tortoise.py | 30 +- examples/quick_tutorial/djangoorm.py | 2 +- examples/quick_tutorial/fastapi.py | 2 +- examples/quick_tutorial/flask.py | 2 +- examples/quick_tutorial/ponyorm.py | 4 +- examples/quick_tutorial/sqlalchemy.py | 14 +- examples/quick_tutorial/tortoise.py | 8 +- fastadmin/__init__.py | 22 +- fastadmin/api/frameworks/django/app/api.py | 20 +- fastadmin/api/frameworks/django/app/urls.py | 7 +- fastadmin/api/frameworks/django/app/views.py | 5 +- fastadmin/api/frameworks/fastapi/api.py | 30 +- fastadmin/api/frameworks/fastapi/app.py | 3 +- fastadmin/api/frameworks/fastapi/views.py | 3 +- fastadmin/api/frameworks/flask/api.py | 27 +- fastadmin/api/frameworks/flask/app.py | 5 +- fastadmin/api/frameworks/flask/views.py | 2 +- fastadmin/api/helpers.py | 16 +- fastadmin/api/schemas.py | 32 +- fastadmin/api/service.py | 30 +- fastadmin/models/base.py | 106 +-- fastadmin/models/helpers.py | 6 +- fastadmin/models/orms/django.py | 8 +- fastadmin/models/orms/ponyorm.py | 6 +- fastadmin/models/orms/sqlalchemy.py | 10 +- fastadmin/models/orms/tortoise.py | 8 +- fastadmin/models/schemas.py | 32 +- fastadmin/settings.py | 48 +- frontend/public/index.html | 4 +- generate_db.py | 4 +- poetry.lock | 194 +---- pyproject.toml | 91 ++- tests/__init__.py | 15 + tests/api/fixtures.py | 49 -- tests/api/frameworks/django/fixtures.py | 15 - tests/api/frameworks/django/test_app.py | 6 +- tests/api/frameworks/fastapi/fixtures.py | 15 - tests/api/frameworks/flask/fixtures.py | 16 - tests/api/frameworks/flask/test_app.py | 6 +- tests/api/test_configuration.py | 39 +- tests/api/test_delete.py | 4 +- tests/api/test_helpers.py | 4 +- tests/api/test_list.py | 9 +- tests/conftest.py | 436 ++++++++++- tests/environment/__init__.py | 7 - .../django/dev}/__init__.py | 0 tests/environment/django/dev/dev/asgi.py | 1 - tests/environment/django/dev/dev/settings.py | 2 +- tests/environment/django/dev/manage.py | 4 - tests/environment/flask/dev.py | 2 +- tests/environment/ponyorm/models.py | 12 +- tests/environment/sqlalchemy/models.py | 24 +- tests/models/fixtures.py | 83 --- tests/models/orms/django/fixtures.py | 48 -- tests/models/orms/ponyorm/__init__.py | 0 tests/models/orms/ponyorm/fixtures.py | 83 --- tests/models/orms/sqlalchemy/__init__.py | 0 tests/models/orms/sqlalchemy/fixtures.py | 104 --- tests/models/orms/tortoiseorm/__init__.py | 0 tests/models/orms/tortoiseorm/fixtures.py | 49 -- tests/models/test_base.py | 2 +- tests/models/test_decorators.py | 4 +- tests/settings.py | 6 +- tests/test_init.py | 2 +- 76 files changed, 1231 insertions(+), 2072 deletions(-) delete mode 100644 .flake8 delete mode 100644 docs/README.md rename {tests/models/orms => docs}/__init__.py (100%) delete mode 100644 tests/api/fixtures.py delete mode 100644 tests/api/frameworks/django/fixtures.py delete mode 100644 tests/api/frameworks/fastapi/fixtures.py delete mode 100644 tests/api/frameworks/flask/fixtures.py rename tests/{models/orms/django => environment/django/dev}/__init__.py (100%) delete mode 100644 tests/models/fixtures.py delete mode 100644 tests/models/orms/django/fixtures.py delete mode 100644 tests/models/orms/ponyorm/__init__.py delete mode 100644 tests/models/orms/ponyorm/fixtures.py delete mode 100644 tests/models/orms/sqlalchemy/__init__.py delete mode 100644 tests/models/orms/sqlalchemy/fixtures.py delete mode 100644 tests/models/orms/tortoiseorm/__init__.py delete mode 100644 tests/models/orms/tortoiseorm/fixtures.py diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 235214c..0000000 --- a/.flake8 +++ /dev/null @@ -1,5 +0,0 @@ -[flake8] -max-line-length = 120 -exclude = .git,.venv,venv,.env,env,__pycache__,__init__.py,.mypy_cache,.pytest_cache,tests -extend-ignore = E203, W503 -per-file-ignores = __init__.py:F401 diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index 656d954..d8954f4 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -19,8 +19,7 @@ jobs: cache: 'yarn' cache-dependency-path: 'frontend/yarn.lock' - name: Install Dependencies - run: | - make install + run: make install - name: Run Lint run: make lint - name: Run Tests diff --git a/Makefile b/Makefile index 0b0a40e..7aeb172 100644 --- a/Makefile +++ b/Makefile @@ -1,84 +1,88 @@ .PHONY: clean clean: - find . -type d -name "__pycache__" -exec rm -rf {} + > /dev/null 2>&1 - find . -type f -name "*.pyc" -exec rm -rf {} + > /dev/null 2>&1 - rm -rf htmlcov - rm -rf .coverage + @exec find . -type d -name "__pycache__" -exec rm -rf {} + > /dev/null 2>&1 + @exec find . -type f -name "*.pyc" -exec rm -rf {} + > /dev/null 2>&1 + @exec rm -rf htmlcov + @exec rm -rf .coverage .PHONY: fix fix: - poetry run pyupgrade --exit-zero-even-if-changed --py39-plus fastadmin/**/*.py tests/**/*.py - poetry run isort --settings-path pyproject.toml fastadmin tests - poetry run black --config pyproject.toml fastadmin tests - cd frontend && make fix + @echo "Run ruff" + @exec poetry run ruff --fix fastadmin tests examples docs + @echo "Run isort" + @exec poetry run isort fastadmin tests examples docs + @echo "Run black" + @exec poetry run black fastadmin tests examples docs + @echo "Run mypy" + @exec poetry run mypy -p fastadmin -p tests -p examples -p docs + @echo "Run frontend linters" + @exec make -C frontend fix .PHONY: lint lint: - poetry run isort --diff --check-only --settings-path pyproject.toml fastadmin tests - poetry run black --diff --check --config pyproject.toml fastadmin tests - poetry run flake8 --show-source --config .flake8 fastadmin tests - poetry run mypy --show-error-code --install-types --non-interactive --namespace-packages --show-traceback --config-file pyproject.toml fastadmin - cd frontend && make lint + @echo "Run ruff" + @exec poetry run ruff fastadmin tests examples docs + @echo "Run isort" + @exec poetry run isort --check-only fastadmin tests examples docs + @echo "Run black" + @exec poetry run black --check --diff fastadmin tests examples docs + @echo "Run mypy" + @exec poetry run mypy -p fastadmin -p tests -p examples -p docs + @echo "Run frontend linters" + @exec make -C frontend lint .PHONY: test test: - poetry run python generate_db.py - ADMIN_ENV_FILE=example.env poetry run pytest --cov=fastadmin --cov-report=term-missing --cov-report=xml --cov-fail-under=90 -s tests - cd frontend && make test + @exec poetry run python generate_db.py + @exec env ADMIN_ENV_FILE=example.env poetry run pytest --cov=fastadmin --cov-report=term-missing --cov-report=xml --cov-fail-under=90 -s tests + @exec make -C frontend test .PHONY: kill kill: - kill -9 $$(lsof -t -i:8090) - kill -9 $$(lsof -t -i:3030) + @exec kill -9 $$(lsof -t -i:8090) + @exec kill -9 $$(lsof -t -i:3030) .PHONY: collectstatic collectstatic: - rm -rf ./fastadmin/static/js - rm -rf ./fastadmin/static/css - cp -rf ./frontend/build/static/js/ ./fastadmin/static/js/ - cp -rf ./frontend/build/static/css/ ./fastadmin/static/css/ - mv fastadmin/static/js/main*.js fastadmin/static/js/main.min.js - mv fastadmin/static/css/main*.css fastadmin/static/css/main.min.css - rm fastadmin/static/js/*.txt + @exec rm -rf ./fastadmin/static/js + @exec rm -rf ./fastadmin/static/css + @exec cp -rf ./frontend/build/static/js/ ./fastadmin/static/js/ + @exec cp -rf ./frontend/build/static/css/ ./fastadmin/static/css/ + @exec mv fastadmin/static/js/main*.js fastadmin/static/js/main.min.js + @exec mv fastadmin/static/css/main*.css fastadmin/static/css/main.min.css + @exec rm fastadmin/static/js/*.txt .PHONY: install install: - poetry install --all-extras - make -C frontend install + @exec poetry install --all-extras + @exec make -C frontend install .PHONY: docs docs: - make -C docs build - cp ./docs/README.md ./README.md + @exec make -C docs build .PHONY: build build: - make docs - make -C frontend build - make collectstatic + @exec make docs + @exec make -C frontend build + @exec make collectstatic .PHONY: pre-commit-install pre-commit-install: - poetry run pip install pre-commit - poetry run pre-commit install + @exec poetry run pip install pre-commit + @exec poetry run pre-commit install .PHONY: pre-commit pre-commit: - poetry run pre-commit run --all-files + @exec poetry run pre-commit run --all-files .PHONY: push -push: - make fix - make lint - make test - make build - make pre-commit - git stash - git checkout main - git pull origin main - git stash pop - git add . - git commit -am "$(message)" - git push origin main +pre-push: + @exec make fix + @exec make lint + @exec make pre-commit-install + @exec make pre-commit + @exec make docs + @exec make build diff --git a/README.md b/README.md index 4ec746d..9133b48 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ Install the package using pip: -Note: For zsh and macos use: pip install fastadmin\[fastapi,django\] +Note: For zsh and macos use: pip install fastadmin[fastapi,django] @@ -314,8 +314,8 @@ Setup FastAdmin for a framework ```python from fastapi import FastAPI -from fastadmin import fastapi_app as admin_app +from fastadmin import fastapi_app as admin_app app = FastAPI() @@ -373,8 +373,8 @@ urlpatterns = [ ```python from flask import Flask -from fastadmin import flask_app as admin_app +from fastadmin import flask_app as admin_app app = Flask(__name__) @@ -433,13 +433,13 @@ Register ORM models ```python -import bcrypt from uuid import UUID -from tortoise.models import Model +import bcrypt from tortoise import fields +from tortoise.models import Model -from fastadmin import register, TortoiseModelAdmin +from fastadmin import TortoiseModelAdmin, register class User(Model): @@ -449,7 +449,7 @@ class User(Model): is_active = fields.BooleanField(default=False) def __str__(self): - return self.username + return self.username @register(User) @@ -501,7 +501,7 @@ class User(models.Model): is_active = models.BooleanField(default=False) def __str__(self): - return self.username + return self.username @register(User) @@ -542,20 +542,14 @@ class UserAdmin(DjangoModelAdmin): ```python import bcrypt -from sqlalchemy import ( - Boolean, - String, - Integer, - select, -) +from sqlalchemy import Boolean, Integer, String, select +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column -from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker from fastadmin import SqlAlchemyModelAdmin, register - sqlalchemy_engine = create_async_engine( - f"sqlite+aiosqlite:///:memory:", + "sqlite+aiosqlite:///:memory:", echo=True, ) sqlalchemy_sessionmaker = async_sessionmaker(sqlalchemy_engine, expire_on_commit=False) @@ -575,7 +569,7 @@ class User(Base): is_active: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) def __str__(self): - return self.username + return self.username @register(User, sqlalchemy_sessionmaker=sqlalchemy_sessionmaker) @@ -628,7 +622,7 @@ db = Database() db.bind(provider="sqlite", filename=":memory:", create_db=True) -class User(db.Entity): +class User(db.Entity): # type: ignore [name-defined] _table_ = "user" id = PrimaryKey(int, auto=True) username = Required(str) @@ -637,7 +631,7 @@ class User(db.Entity): is_active = Required(bool, default=False) def __str__(self): - return self.username + return self.username @register(User) diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 4ec746d..0000000 --- a/docs/README.md +++ /dev/null @@ -1,687 +0,0 @@ -## Admin Dashboard App for FastAPI/Flask/Django - -[![Build Status](https://github.com/vsdudakov/fastadmin/workflows/CI/badge.svg?branch=main)](https://github.com/vsdudakov/fastadmin/workflows/CI/badge.svg?branch=main) -[![codecov](https://codecov.io/gh/vsdudakov/fastadmin/branch/main/graph/badge.svg?token=RNGX5HOW3T)](https://codecov.io/gh/vsdudakov/fastadmin) -[![License](https://img.shields.io/github/license/vsdudakov/fastadmin)](https://github.com/vsdudakov/fastadmin/blob/master/LICENSE) -[![PyPi](https://badgen.net/pypi/v/fastadmin)](https://pypi.org/project/fastadmin/) -[![Python 3.10](https://img.shields.io/badge/python-3.10-blue.svg)](https://www.python.org/downloads/release/python-3100/) - -## Screenshots - -![SignIn View](https://raw.githubusercontent.com/vsdudakov/fastadmin/main/docs/assets/images/signin.png) -![List View](https://raw.githubusercontent.com/vsdudakov/fastadmin/main/docs/assets/images/list.png) -![Change View](https://raw.githubusercontent.com/vsdudakov/fastadmin/main/docs/assets/images/change.png) - -

- - tweet - -

- - - -## Introduction - - -FastAdmin is an easy-to-use Admin Dashboard App for FastAPI/Django/Flask inspired by Django Admin. - - - - - - - - - - - - - - - -FastAdmin was built with relations in mind and admiration for the excellent and popular Django Admin. It's engraved in its design that you may configure your admin dashboard for FastAPI/Django/Flask easiest way. - - - - - - - - - - - - - - - -FastAdmin is designed to be minimalistic, functional and yet familiar. - - - - - - - - - - - - - - - - - - -## Getting Started - - - - - - -If you have any questions that are beyond the scope of the documentation, Please feel free to email us. - - - - - - - - - - - - -### Installation - - - - -Follow the steps below to setup FastAdmin: - - - - - - - - - - - - - -Install the package using pip: - - - - - - - - - - - - - - - - - - - -Note: For zsh and macos use: pip install fastadmin\[fastapi,django\] - - - - - - - - - - - - - - - - - - - -```bash - -pip install fastadmin[fastapi,django] # for fastapi with django orm -pip install fastadmin[fastapi,tortoise-orm] # for fastapi with tortoise orm -pip install fastadmin[fastapi,pony] # for fastapi with pony orm -pip install fastadmin[fastapi,sqlalchemy] # for fastapi with sqlalchemy orm -pip install fastadmin[django] # for django with django orm -pip install fastadmin[django,pony] # for django with pony orm -pip install fastadmin[flask,sqlalchemy] # for flask with sqlalchemy - -``` - - - - - - - -Install the package using poetry: - - - - - - - - - - - - - - - - - - - - - - - -```bash - -poetry add 'fastadmin[fastapi,django]' # for fastapi with django orm -poetry add 'fastadmin[fastapi,tortoise-orm]' # for fastapi with tortoise orm -poetry add 'fastadmin[fastapi,pony]' # for fastapi with pony orm -poetry add 'fastadmin[fastapi,sqlalchemy]' # for fastapi with sqlalchemy orm -poetry add 'fastadmin[django]' # for django with django orm -poetry add 'fastadmin[django,pony]' # for django with pony orm -poetry add 'fastadmin[flask,sqlalchemy]' # for flask with sqlalchemy - -``` - - - - - - - -Configure required settings using virtual environment variables: - - - - - - - - - - - - - - - - - - - -Note: You can add these variables to .env and use python-dotenv to load them. See all settings here - - - - - - - - - - - - - - - - - - - -```bash - -export ADMIN_USER_MODEL=User -export ADMIN_USER_MODEL_USERNAME_FIELD=username -export ADMIN_SECRET_KEY=secret_key - -``` - - - - - - - -### Quick Tutorial - - - - -Setup FastAdmin for a framework - - - - - - - - - - - - - - - - - - - - - - - - - - -### FastAPI - - - - - - - - - - - - -```python -from fastapi import FastAPI -from fastadmin import fastapi_app as admin_app - - -app = FastAPI() - -app.mount("/admin", admin_app) - -``` - - - - - - -### Django - - - - - - - - - - - - -```python -from django.urls import path - -from fastadmin import get_django_admin_urls as get_admin_urls -from fastadmin.settings import settings - -urlpatterns = [ - path(f"{settings.ADMIN_PREFIX}/", get_admin_urls()), -] - -``` - - - - - - -### Flask - - - - - - - - - - - - -```python -from flask import Flask -from fastadmin import flask_app as admin_app - - -app = Flask(__name__) - -app.register_blueprint(admin_app, url_prefix="/admin") - -``` - - - - - - - - - - - -Register ORM models - - - - - - - - - - - - - - - - - - - - - - - - - - -### Tortoise ORM - - - - - - - - - - - - -```python -import bcrypt -from uuid import UUID - -from tortoise.models import Model -from tortoise import fields - -from fastadmin import register, TortoiseModelAdmin - - -class User(Model): - username = fields.CharField(max_length=255, unique=True) - hash_password = fields.CharField(max_length=255) - is_superuser = fields.BooleanField(default=False) - is_active = fields.BooleanField(default=False) - - def __str__(self): - return self.username - - -@register(User) -class UserAdmin(TortoiseModelAdmin): - exclude = ("hash_password",) - list_display = ("id", "username", "is_superuser", "is_active") - list_display_links = ("id", "username") - list_filter = ("id", "username", "is_superuser", "is_active") - search_fields = ("username",) - - async def authenticate(self, username: str, password: str) -> UUID | int | None: - user = await User.filter(username=username, is_superuser=True).first() - if not user: - return None - if not bcrypt.checkpw(password.encode(), user.hash_password.encode()): - return None - return user.id - -``` - - - - - - -### Django ORM - - - - - - - - - - - - -```python -from django.db import models - -from fastadmin import DjangoModelAdmin, register - - -class User(models.Model): - username = models.CharField(max_length=255, unique=True) - hash_password = models.CharField(max_length=255) - is_superuser = models.BooleanField(default=False) - is_active = models.BooleanField(default=False) - - def __str__(self): - return self.username - - -@register(User) -class UserAdmin(DjangoModelAdmin): - exclude = ("hash_password",) - list_display = ("id", "username", "is_superuser", "is_active") - list_display_links = ("id", "username") - list_filter = ("id", "username", "is_superuser", "is_active") - search_fields = ("username",) - - def authenticate(self, username, password): - obj = User.objects.filter(username=username, is_superuser=True).first() - if not obj: - return None - if not obj.check_password(password): - return None - return obj.id - -``` - - - - - - -### SQL Alchemy - - - - - - - - - - - - -```python -import bcrypt -from sqlalchemy import ( - Boolean, - String, - Integer, - select, -) -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column -from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker - -from fastadmin import SqlAlchemyModelAdmin, register - - -sqlalchemy_engine = create_async_engine( - f"sqlite+aiosqlite:///:memory:", - echo=True, -) -sqlalchemy_sessionmaker = async_sessionmaker(sqlalchemy_engine, expire_on_commit=False) - - -class Base(DeclarativeBase): - pass - - -class User(Base): - __tablename__ = "user" - - id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False) - username: Mapped[str] = mapped_column(String(length=255), nullable=False) - hash_password: Mapped[str] = mapped_column(String(length=255), nullable=False) - is_superuser: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) - is_active: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) - - def __str__(self): - return self.username - - -@register(User, sqlalchemy_sessionmaker=sqlalchemy_sessionmaker) -class UserAdmin(SqlAlchemyModelAdmin): - exclude = ("hash_password",) - list_display = ("id", "username", "is_superuser", "is_active") - list_display_links = ("id", "username") - list_filter = ("id", "username", "is_superuser", "is_active") - search_fields = ("username",) - - async def authenticate(self, username, password): - sessionmaker = self.get_sessionmaker() - async with sessionmaker() as session: - query = select(User).filter_by(username=username, password=password, is_superuser=True) - result = await session.scalars(query) - user = result.first() - if not user: - return None - if not bcrypt.checkpw(password.encode(), user.hash_password.encode()): - return None - return user.id - -``` - - - - - - -### Pony ORM - - - - - - - - - - - - -```python -import bcrypt -from pony.orm import Database, PrimaryKey, Required, db_session - -from fastadmin import PonyORMModelAdmin, register - -db = Database() -db.bind(provider="sqlite", filename=":memory:", create_db=True) - - -class User(db.Entity): - _table_ = "user" - id = PrimaryKey(int, auto=True) - username = Required(str) - hash_password = Required(str) - is_superuser = Required(bool, default=False) - is_active = Required(bool, default=False) - - def __str__(self): - return self.username - - -@register(User) -class UserAdmin(PonyORMModelAdmin): - exclude = ("hash_password",) - list_display = ("id", "username", "is_superuser", "is_active") - list_display_links = ("id", "username") - list_filter = ("id", "username", "is_superuser", "is_active") - search_fields = ("username",) - - @db_session - def authenticate(self, username, password): - user = next((f for f in self.model_cls.select(username=username, password=password, is_superuser=True)), None) - if not user: - return None - if not bcrypt.checkpw(password.encode(), user.hash_password.encode()): - return None - return user.id - -``` - - - - - - - - - - - - - - - - - - - - - - -## Documentation -See full documentation [here](https://vsdudakov.github.io/fastadmin). - -## License -This project is licensed under the MIT License - see the [LICENSE](https://github.com/vsdudakov/fastadmin/blob/main/LICENSE) file for details. diff --git a/tests/models/orms/__init__.py b/docs/__init__.py similarity index 100% rename from tests/models/orms/__init__.py rename to docs/__init__.py diff --git a/docs/build.py b/docs/build.py index 9c26062..2f30b4c 100644 --- a/docs/build.py +++ b/docs/build.py @@ -1,25 +1,27 @@ -from jinja2 import Environment, FileSystemLoader, select_autoescape -from datetime import date -from htmlmin import minify import inspect import os import sys +from datetime import datetime, timezone +from pathlib import Path -from tests.settings import ROOT_DIR +from django.apps import apps +from django.apps.registry import Apps +from htmlmin import minify +from jinja2 import Environment, FileSystemLoader, select_autoescape -# Settings -os.environ.setdefault("ADMIN_ENV_FILE", os.path.join(os.path.dirname(__file__), "..", "example.env")) -from fastadmin.settings import Settings +from fastadmin.settings import ROOT_DIR, Settings +from tests.settings import ROOT_DIR as TESTS_ROOT_DIR + +os.environ.setdefault("ADMIN_ENV_FILE", str(ROOT_DIR / "example.env")) +sys.path.append(str(TESTS_ROOT_DIR / "environment" / "django" / "dev")) -# FAKE Django -sys.path.append(os.path.join(ROOT_DIR, "environment", "django", "dev")) # for dev.settings -from django.apps.registry import Apps -from django.apps import apps -from django.conf import settings -settings.configure() Apps.check_apps_ready = lambda x: None + + class App: label = "app" + + apps.get_containing_app_config = lambda x: App() @@ -36,6 +38,18 @@ def read_cls_docstring(cls): def get_versions(): return [ + { + "version": "0.1.40", + "changes": [ + "Added RUFF linter. Cleaned up code. Removed pydantic dependency.", + ], + }, + { + "version": "0.1.39", + "changes": [ + "Bug fixes.", + ], + }, { "version": "0.1.38", "changes": [ @@ -106,7 +120,7 @@ def get_sections(): "name": "Quick Tutorial", "url": "#quick_tutorial", }, - ] + ], }, { "name": "Settings", @@ -128,7 +142,7 @@ def get_sections(): "name": "Chart Types", "url": "#widget-chart-types", }, - ] + ], }, { "name": "Model Admins", @@ -150,7 +164,7 @@ def get_sections(): "name": "Form Field Types", "url": "#model-form-field-types", }, - ] + ], }, { "name": "Inline Model Admins", @@ -164,7 +178,7 @@ def get_sections(): "name": "Methods and Attributes", "url": "#inline-methods-and-attributes", }, - ] + ], }, { "name": "Changelog", @@ -173,28 +187,28 @@ def get_sections(): { "name": f"v{version['version']}", "url": f"#v{version['version'].replace('.', '_')}", - } for version in versions - ] + } + for version in versions + ], }, - ] def get_page_context(page_url): - from examples.quick_tutorial import fastapi as quick_tutorial_fastapi + from examples.dashboard import djangoorm as dashboard_djangoorm + from examples.dashboard import tortoise as dashboard_tortoise + from examples.inlines import tortoise as inlines_tortoise + from examples.models import tortoise as models_tortoise from examples.quick_tutorial import django as quick_tutorial_django - from examples.quick_tutorial import flask as quick_tutorial_flask - from examples.quick_tutorial import tortoise as quick_tutorial_tortoise from examples.quick_tutorial import djangoorm as quick_tutorial_djangoorm - from examples.quick_tutorial import sqlalchemy as quick_tutorial_sqlalchemy + from examples.quick_tutorial import fastapi as quick_tutorial_fastapi + from examples.quick_tutorial import flask as quick_tutorial_flask from examples.quick_tutorial import ponyorm as quick_tutorial_ponyorm - from examples.dashboard import tortoise as dashboard_tortoise - from examples.dashboard import djangoorm as dashboard_djangoorm - from examples.models import tortoise as models_tortoise - from examples.inlines import tortoise as inlines_tortoise + from examples.quick_tutorial import sqlalchemy as quick_tutorial_sqlalchemy + from examples.quick_tutorial import tortoise as quick_tutorial_tortoise + from fastadmin import DashboardWidgetAdmin, DashboardWidgetType, InlineModelAdmin, ModelAdmin, WidgetType from fastadmin.models.base import BaseModelAdmin - from fastadmin import DashboardWidgetAdmin, DashboardWidgetType, ModelAdmin, InlineModelAdmin, WidgetType match page_url: case "#introduction": @@ -231,12 +245,11 @@ def get_page_context(page_url): }, { "type": "alert-info", - "content": "Note: For zsh and macos use: pip install fastadmin\[fastapi,django\]", + "content": "Note: For zsh and macos use: pip install fastadmin[fastapi,django]", }, { "type": "code-bash", - "content": -""" + "content": """ pip install fastadmin[fastapi,django] # for fastapi with django orm pip install fastadmin[fastapi,tortoise-orm] # for fastapi with tortoise orm pip install fastadmin[fastapi,pony] # for fastapi with pony orm @@ -244,7 +257,7 @@ def get_page_context(page_url): pip install fastadmin[django] # for django with django orm pip install fastadmin[django,pony] # for django with pony orm pip install fastadmin[flask,sqlalchemy] # for flask with sqlalchemy -""" +""", }, { "type": "text", @@ -252,8 +265,7 @@ def get_page_context(page_url): }, { "type": "code-bash", - "content": -""" + "content": """ poetry add 'fastadmin[fastapi,django]' # for fastapi with django orm poetry add 'fastadmin[fastapi,tortoise-orm]' # for fastapi with tortoise orm poetry add 'fastadmin[fastapi,pony]' # for fastapi with pony orm @@ -261,7 +273,7 @@ def get_page_context(page_url): poetry add 'fastadmin[django]' # for django with django orm poetry add 'fastadmin[django,pony]' # for django with pony orm poetry add 'fastadmin[flask,sqlalchemy]' # for flask with sqlalchemy -""" +""", }, { "type": "text", @@ -273,12 +285,11 @@ def get_page_context(page_url): }, { "type": "code-bash", - "content": -""" + "content": """ export ADMIN_USER_MODEL=User export ADMIN_USER_MODEL_USERNAME_FIELD=username export ADMIN_SECRET_KEY=secret_key -""" +""", }, ] case "#quick_tutorial": @@ -294,34 +305,19 @@ def get_page_context(page_url): { "name": "FastAPI", "id": "fastapi", - "content": [ - { - "type": "code-python", - "content": inspect.getsource(quick_tutorial_fastapi) - } - ] + "content": [{"type": "code-python", "content": inspect.getsource(quick_tutorial_fastapi)}], }, { "name": "Django", "id": "django", - "content": [ - { - "type": "code-python", - "content": inspect.getsource(quick_tutorial_django) - } - ] + "content": [{"type": "code-python", "content": inspect.getsource(quick_tutorial_django)}], }, { "name": "Flask", "id": "flask", - "content": [ - { - "type": "code-python", - "content": inspect.getsource(quick_tutorial_flask) - } - ] - } - ] + "content": [{"type": "code-python", "content": inspect.getsource(quick_tutorial_flask)}], + }, + ], }, { "type": "text-lead", @@ -334,44 +330,28 @@ def get_page_context(page_url): { "name": "Tortoise ORM", "id": "tortoise_orm", - "content": [ - { - "type": "code-python", - "content": inspect.getsource(quick_tutorial_tortoise) - } - ] + "content": [{"type": "code-python", "content": inspect.getsource(quick_tutorial_tortoise)}], }, { "name": "Django ORM", "id": "django_orm", "content": [ - { - "type": "code-python", - "content": inspect.getsource(quick_tutorial_djangoorm) - } - ] + {"type": "code-python", "content": inspect.getsource(quick_tutorial_djangoorm)} + ], }, { "name": "SQL Alchemy", "id": "sql_alchemy", "content": [ - { - "type": "code-python", - "content": inspect.getsource(quick_tutorial_sqlalchemy) - } - ] + {"type": "code-python", "content": inspect.getsource(quick_tutorial_sqlalchemy)} + ], }, { "name": "Pony ORM", "id": "pony_orm", - "content": [ - { - "type": "code-python", - "content": inspect.getsource(quick_tutorial_ponyorm) - } - ] - } - ] + "content": [{"type": "code-python", "content": inspect.getsource(quick_tutorial_ponyorm)}], + }, + ], }, ] # settings @@ -385,10 +365,7 @@ def get_page_context(page_url): "type": "alert-info", "content": "Note: Export virtual environment variables or create .env file with variables and use python-dotenv package.", }, - { - "type": "code-python", - "content": inspect.getsource(Settings) - }, + {"type": "code-python", "content": inspect.getsource(Settings)}, { "type": "alert-warning", "content": "Note: Settings without default values are required.", @@ -408,44 +385,24 @@ def get_page_context(page_url): { "name": "Tortoise ORM", "id": "dashboard_tortoise_orm", - "content": [ - { - "type": "code-python", - "content": inspect.getsource(dashboard_tortoise) - } - ] + "content": [{"type": "code-python", "content": inspect.getsource(dashboard_tortoise)}], }, { "name": "Django ORM", "id": "dashboard_django_orm", - "content": [ - { - "type": "code-python", - "content": inspect.getsource(dashboard_djangoorm) - } - ] + "content": [{"type": "code-python", "content": inspect.getsource(dashboard_djangoorm)}], }, { "name": "SQL Alchemy", "id": "dashboard_sql_alchemy", - "content": [ - { - "type": "code-python", - "content": "See example for Tortoise ORM" - } - ] + "content": [{"type": "code-python", "content": "See example for Tortoise ORM"}], }, { "name": "Pony ORM", "id": "dashboard_pony_orm", - "content": [ - { - "type": "code-python", - "content": "See example for Tortoise ORM" - } - ] - } - ] + "content": [{"type": "code-python", "content": "See example for Tortoise ORM"}], + }, + ], }, ] case "#widget-methods-and-attributes": @@ -454,10 +411,7 @@ def get_page_context(page_url): "type": "text", "content": "There are methods and attributes for Dashboard Widget Admin:", }, - { - "type": "code-python", - "content": inspect.getsource(DashboardWidgetAdmin) - }, + {"type": "code-python", "content": inspect.getsource(DashboardWidgetAdmin)}, { "type": "alert-warning", "content": "Note: Please see antd charts for x_field_filter_widget_props.", @@ -469,10 +423,7 @@ def get_page_context(page_url): "type": "text", "content": "There are widget types which fastadmin dashboard supports:", }, - { - "type": "code-python", - "content": inspect.getsource(DashboardWidgetType) - }, + {"type": "code-python", "content": inspect.getsource(DashboardWidgetType)}, { "type": "alert-warning", "content": "Note: Please see antd charts for more details (e.g. to see how they look like).", @@ -488,44 +439,24 @@ def get_page_context(page_url): { "name": "Tortoise ORM", "id": "models_tortoise_orm", - "content": [ - { - "type": "code-python", - "content": inspect.getsource(models_tortoise) - } - ] + "content": [{"type": "code-python", "content": inspect.getsource(models_tortoise)}], }, { "name": "Django ORM", "id": "models_django_orm", - "content": [ - { - "type": "alert-info", - "content": "See example for Tortoise ORM" - } - ] + "content": [{"type": "alert-info", "content": "See example for Tortoise ORM"}], }, { "name": "SQL Alchemy", "id": "models_sql_alchemy", - "content": [ - { - "type": "alert-info", - "content": "See example for Tortoise ORM" - } - ] + "content": [{"type": "alert-info", "content": "See example for Tortoise ORM"}], }, { "name": "Pony ORM", "id": "models_pony_orm", - "content": [ - { - "type": "alert-info", - "content": "See example for Tortoise ORM" - } - ] - } - ] + "content": [{"type": "alert-info", "content": "See example for Tortoise ORM"}], + }, + ], }, ] case "#authentication": @@ -541,18 +472,12 @@ def get_page_context(page_url): "type": "text", "content": "There are methods and attributes for Model Admin:", }, - { - "type": "code-python", - "content": inspect.getsource(BaseModelAdmin) - }, + {"type": "code-python", "content": inspect.getsource(BaseModelAdmin)}, { "type": "text", "content": "Specific methods and attributes for Model Admin:", }, - { - "type": "code-python", - "content": inspect.getsource(ModelAdmin) - }, + {"type": "code-python", "content": inspect.getsource(ModelAdmin)}, ] case "#model-form-field-types": return [ @@ -560,10 +485,7 @@ def get_page_context(page_url): "type": "text", "content": "There are form field types for model admin:", }, - { - "type": "code-python", - "content": inspect.getsource(WidgetType) - }, + {"type": "code-python", "content": inspect.getsource(WidgetType)}, { "type": "alert-warning", "content": "Note: Please see antd components for more details (e.g. to see how they look like).", @@ -579,44 +501,24 @@ def get_page_context(page_url): { "name": "Tortoise ORM", "id": "inlines_tortoise_orm", - "content": [ - { - "type": "code-python", - "content": inspect.getsource(inlines_tortoise) - } - ] + "content": [{"type": "code-python", "content": inspect.getsource(inlines_tortoise)}], }, { "name": "Django ORM", "id": "inlines_django_orm", - "content": [ - { - "type": "alert-info", - "content": "See example for Tortoise ORM" - } - ] + "content": [{"type": "alert-info", "content": "See example for Tortoise ORM"}], }, { "name": "SQL Alchemy", "id": "inlines_sql_alchemy", - "content": [ - { - "type": "alert-info", - "content": "See example for Tortoise ORM" - } - ] + "content": [{"type": "alert-info", "content": "See example for Tortoise ORM"}], }, { "name": "Pony ORM", "id": "inlines_pony_orm", - "content": [ - { - "type": "alert-info", - "content": "See example for Tortoise ORM" - } - ] - } - ] + "content": [{"type": "alert-info", "content": "See example for Tortoise ORM"}], + }, + ], }, ] case "#inline-methods-and-attributes": @@ -633,10 +535,7 @@ def get_page_context(page_url): "type": "text", "content": "Specific methods and attributes for Inline Model Admin:", }, - { - "type": "code-python", - "content": inspect.getsource(InlineModelAdmin) - }, + {"type": "code-python", "content": inspect.getsource(InlineModelAdmin)}, ] # changelog case "#changelog": @@ -655,7 +554,8 @@ def get_page_context(page_url): { "type": "text", "content": change, - } for change in v["changes"] + } + for change in v["changes"] ] return [] @@ -669,9 +569,9 @@ def get_context(): "email": AUTHOR_EMAIL, }, "name": NAME, - "year": date.today().year, + "year": datetime.now(timezone.utc).year, "created_date": "7 March 2023", - "updated_date": date.today().strftime("%d %B %Y"), + "updated_date": datetime.now(timezone.utc).strftime("%d %B %Y"), "github_url": GITHUB_URL, "pypi_url": PYPI_URL, "versions": get_versions(), @@ -681,24 +581,19 @@ def get_context(): def build(): - env = Environment( - loader=FileSystemLoader("."), - autoescape=select_autoescape() - ) + env = Environment(loader=FileSystemLoader("."), autoescape=select_autoescape()) context = get_context() index_template = env.get_template("templates/index.html") index_html = index_template.render(**context) - with open("index.html", "w") as fh: + with Path.open(Path("index.html"), "w") as fh: fh.write(minify(index_html)) readme_template = env.get_template("templates/readme.md") readme_md = readme_template.render(**context) - with open("README.md", "w") as fh: + with Path.open(Path(ROOT_DIR / ".." / "README.md"), "w") as fh: fh.write(readme_md) - - if __name__ == "__main__": build() diff --git a/docs/index.html b/docs/index.html index 7f395a2..acbd2cc 100644 --- a/docs/index.html +++ b/docs/index.html @@ -2,7 +2,7 @@
  • - -->

    FastAdmin | Documentation

    • Created: 7 March 2023
    • Updated: 22 April 2023

    Introduction

    FastAdmin is an easy-to-use Admin Dashboard App for FastAPI/Django/Flask inspired by Django Admin.

    FastAdmin was built with relations in mind and admiration for the excellent and popular Django Admin. It's engraved in its design that you may configure your admin dashboard for FastAPI/Django/Flask easiest way.

    FastAdmin is designed to be minimalistic, functional and yet familiar.


    Getting Started

    If you have any questions that are beyond the scope of the documentation, Please feel free to email us.

    Installation

    Follow the steps below to setup FastAdmin:

    Install the package using pip:

    Note: For zsh and macos use: pip install fastadmin\[fastapi,django\]

    +              -->  

    FastAdmin | Documentation

    • Created: 7 March 2023
    • Updated: 10 June 2023

    Introduction

    FastAdmin is an easy-to-use Admin Dashboard App for FastAPI/Django/Flask inspired by Django Admin.

    FastAdmin was built with relations in mind and admiration for the excellent and popular Django Admin. It's engraved in its design that you may configure your admin dashboard for FastAPI/Django/Flask easiest way.

    FastAdmin is designed to be minimalistic, functional and yet familiar.


    Getting Started

    If you have any questions that are beyond the scope of the documentation, Please feel free to email us.

    Installation

    Follow the steps below to setup FastAdmin:

    Install the package using pip:

    Note: For zsh and macos use: pip install fastadmin[fastapi,django]

       
     
     pip install fastadmin[fastapi,django]  # for fastapi with django orm
    @@ -37,8 +37,8 @@
     

    Quick Tutorial

    Setup FastAdmin for a framework

       
     from fastapi import FastAPI
    -from fastadmin import fastapi_app as admin_app
     
    +from fastadmin import fastapi_app as admin_app
     
     app = FastAPI()
     
    @@ -60,8 +60,8 @@
     
       
     from flask import Flask
    -from fastadmin import flask_app as admin_app
     
    +from fastadmin import flask_app as admin_app
     
     app = Flask(__name__)
     
    @@ -70,13 +70,13 @@
       
     

    Register ORM models

       
    -import bcrypt
     from uuid import UUID
     
    -from tortoise.models import Model
    +import bcrypt
     from tortoise import fields
    +from tortoise.models import Model
     
    -from fastadmin import register, TortoiseModelAdmin
    +from fastadmin import TortoiseModelAdmin, register
     
     
     class User(Model):
    @@ -86,7 +86,7 @@
         is_active = fields.BooleanField(default=False)
     
         def __str__(self):
    -      return self.username
    +        return self.username
     
     
     @register(User)
    @@ -120,7 +120,7 @@
         is_active = models.BooleanField(default=False)
     
         def __str__(self):
    -      return self.username
    +        return self.username
     
     
     @register(User)
    @@ -143,20 +143,14 @@
     
       
     import bcrypt
    -from sqlalchemy import (
    -    Boolean,
    -    String,
    -    Integer,
    -    select,
    -)
    +from sqlalchemy import Boolean, Integer, String, select
    +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
     from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
    -from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
     
     from fastadmin import SqlAlchemyModelAdmin, register
     
    -
     sqlalchemy_engine = create_async_engine(
    -    f"sqlite+aiosqlite:///:memory:",
    +    "sqlite+aiosqlite:///:memory:",
         echo=True,
     )
     sqlalchemy_sessionmaker = async_sessionmaker(sqlalchemy_engine, expire_on_commit=False)
    @@ -176,7 +170,7 @@
         is_active: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
     
         def __str__(self):
    -      return self.username
    +        return self.username
     
     
     @register(User, sqlalchemy_sessionmaker=sqlalchemy_sessionmaker)
    @@ -211,7 +205,7 @@
     db.bind(provider="sqlite", filename=":memory:", create_db=True)
     
     
    -class User(db.Entity):
    +class User(db.Entity):  # type: ignore [name-defined]
         _table_ = "user"
         id = PrimaryKey(int, auto=True)
         username = Required(str)
    @@ -220,7 +214,7 @@
         is_active = Required(bool, default=False)
     
         def __str__(self):
    -      return self.username
    +        return self.username
     
     
     @register(User)
    @@ -243,61 +237,61 @@
       
     

    Settings

    There are settings with default values:

    Note: Export virtual environment variables or create .env file with variables and use python-dotenv package.

       
    -class Settings(BaseSettings):
    +class Settings:
         """Settings"""
     
         # This value is the prefix you used for mounting FastAdmin app for FastAPI.
    -    ADMIN_PREFIX: str = "admin"
    +    ADMIN_PREFIX: str = os.getenv("ADMIN_PREFIX", "admin")
     
         # This value is the site name on sign-in page and on header.
    -    ADMIN_SITE_NAME: str = "FastAdmin"
    +    ADMIN_SITE_NAME: str = os.getenv("ADMIN_SITE_NAME", "FastAdmin")
     
         # This value is the logo path on sign-in page.
    -    ADMIN_SITE_SIGN_IN_LOGO: str = "/admin/static/images/sign-in-logo.svg"
    +    ADMIN_SITE_SIGN_IN_LOGO: str = os.getenv("ADMIN_SITE_SIGN_IN_LOGO", "/admin/static/images/sign-in-logo.svg")
     
         # This value is the logo path on header.
    -    ADMIN_SITE_HEADER_LOGO: str = "/admin/static/images/header-logo.svg"
    +    ADMIN_SITE_HEADER_LOGO: str = os.getenv("ADMIN_SITE_HEADER_LOGO", "/admin/static/images/header-logo.svg")
     
         # This value is the favicon path.
    -    ADMIN_SITE_FAVICON: str = "/admin/static/images/favicon.png"
    +    ADMIN_SITE_FAVICON: str = os.getenv("ADMIN_SITE_FAVICON", "/admin/static/images/favicon.png")
     
         # This value is the primary color for FastAdmin.
    -    ADMIN_PRIMARY_COLOR: str = "#009485"
    +    ADMIN_PRIMARY_COLOR: str = os.getenv("ADMIN_PRIMARY_COLOR", "#009485")
     
         # This value is the session id key to store session id in http only cookies.
    -    ADMIN_SESSION_ID_KEY: str = "admin_session_id"
    +    ADMIN_SESSION_ID_KEY: str = os.getenv("ADMIN_SESSION_ID_KEY", "admin_session_id")
     
         # This value is the expired_at period (in sec) for session id.
    -    ADMIN_SESSION_EXPIRED_AT: int = 144000  # in sec
    +    ADMIN_SESSION_EXPIRED_AT: int = os.getenv("ADMIN_SESSION_EXPIRED_AT", 144000)  # in sec
    +
    +    # This value is the date format for JS widgets.
    +    ADMIN_DATE_FORMAT: str = os.getenv("ADMIN_DATE_FORMAT", "YYYY-MM-DD")
    +
    +    # This value is the datetime format for JS widgets.
    +    ADMIN_DATETIME_FORMAT: str = os.getenv("ADMIN_DATETIME_FORMAT", "YYYY-MM-DD HH:mm")
    +
    +    # This value is the time format for JS widgets.
    +    ADMIN_TIME_FORMAT: str = os.getenv("ADMIN_TIME_FORMAT", "HH:mm:ss")
     
         # This value is the name for User db/orm model class for authentication.
    -    ADMIN_USER_MODEL: str
    +    ADMIN_USER_MODEL: str = os.getenv("ADMIN_USER_MODEL")
     
         # This value is the username field for User db/orm model for for authentication.
    -    ADMIN_USER_MODEL_USERNAME_FIELD: str
    +    ADMIN_USER_MODEL_USERNAME_FIELD: str = os.getenv("ADMIN_USER_MODEL_USERNAME_FIELD")
     
         # This value is the key to securing signed data - it is vital you keep this secure,
         # or attackers could use it to generate their own signed values.
    -    ADMIN_SECRET_KEY: str
    -
    -    # This value is the date format for JS widgets.
    -    ADMIN_DATE_FORMAT: str = "YYYY-MM-DD"
    -
    -    # This value is the datetime format for JS widgets.
    -    ADMIN_DATETIME_FORMAT: str = "YYYY-MM-DD HH:mm"
    -
    -    # This value is the time format for JS widgets.
    -    ADMIN_TIME_FORMAT: str = "HH:mm:ss"
    +    ADMIN_SECRET_KEY: str = os.getenv("ADMIN_SECRET_KEY")
     
       
     

    Note: Settings without default values are required.


    Dashboard Widget Admins

    Registering Widgets

    Register Dashboard widgets

       
    -from tortoise import Tortoise
    +from datetime import datetime, timedelta, timezone
    +
    +from tortoise import Tortoise, fields
     from tortoise.models import Model
    -from tortoise import fields
    -from datetime import datetime, timedelta
     
    -from fastadmin import register_widget, DashboardWidgetAdmin, DashboardWidgetType, WidgetType
    +from fastadmin import DashboardWidgetAdmin, DashboardWidgetType, WidgetType, register_widget
     
     
     class DashboardUser(Model):
    @@ -307,7 +301,7 @@
         is_active = fields.BooleanField(default=False)
     
         def __str__(self):
    -      return self.username
    +        return self.username
     
     
     @register_widget
    @@ -329,11 +323,11 @@
             conn = Tortoise.get_connection("default")
     
             if not min_x_field:
    -            min_x_field_date = datetime.utcnow() - timedelta(days=360)
    +            min_x_field_date = datetime.now(timezone.utc) - timedelta(days=360)
             else:
                 min_x_field_date = datetime.fromisoformat(min_x_field.replace("Z", "+00:00"))
             if not max_x_field:
    -            max_x_field_date = datetime.utcnow() + timedelta(days=1)
    +            max_x_field_date = datetime.now(timezone.utc) + timedelta(days=1)
             else:
                 max_x_field_date = datetime.fromisoformat(max_x_field.replace("Z", "+00:00"))
     
    @@ -361,12 +355,12 @@
       
     
       
    -from django.db import models
    -from django.db import connection
    -from datetime import datetime, timedelta
    +from datetime import datetime, timedelta, timezone
    +from typing import Any
     
    -from fastadmin import register_widget, DashboardWidgetAdmin, DashboardWidgetType, WidgetType
    +from django.db import connection, models
     
    +from fastadmin import DashboardWidgetAdmin, DashboardWidgetType, WidgetType, register_widget
     
     
     class DashboardUser(models.Model):
    @@ -376,7 +370,7 @@
         is_active = models.BooleanField(default=False)
     
         def __str__(self):
    -      return self.username
    +        return self.username
     
     
     @register_widget
    @@ -389,23 +383,23 @@
         x_field_filter_widget_props = {"picker": "month"}
         x_field_periods = ["day", "week", "month", "year"]
     
    -    def get_data(
    +    def get_data(  # type: ignore [override]
             self,
             min_x_field: str | None = None,
             max_x_field: str | None = None,
             period_x_field: str | None = None,
    -    ) -> dict:
    +    ) -> dict[str, Any]:
             def dictfetchall(cursor):
                 columns = [col[0] for col in cursor.description]
    -            return [dict(zip(columns, row)) for row in cursor.fetchall()]
    +            return [dict(zip(columns, row, strict=True)) for row in cursor.fetchall()]
     
             with connection.cursor() as c:
                 if not min_x_field:
    -                min_x_field_date = datetime.utcnow() - timedelta(days=360)
    +                min_x_field_date = datetime.now(timezone.utc) - timedelta(days=360)
                 else:
                     min_x_field_date = datetime.fromisoformat(min_x_field.replace("Z", "+00:00"))
                 if not max_x_field:
    -                max_x_field_date = datetime.utcnow() + timedelta(days=1)
    +                max_x_field_date = datetime.now(timezone.utc) + timedelta(days=1)
                 else:
                     max_x_field_date = datetime.fromisoformat(max_x_field.replace("Z", "+00:00"))
     
    @@ -482,11 +476,13 @@
       
     

    Note: Please see antd charts for more details (e.g. to see how they look like).


    Model Admins

    Registering Models

       
    -from tortoise.models import Model
    -from tortoise import fields
    +from uuid import UUID
    +
     import bcrypt
    +from tortoise import fields
    +from tortoise.models import Model
     
    -from fastadmin import register, TortoiseModelAdmin, action, WidgetType
    +from fastadmin import TortoiseModelAdmin, WidgetType, action, register
     
     
     class ModelUser(Model):
    @@ -496,15 +492,22 @@
         is_active = fields.BooleanField(default=False)
     
         def __str__(self):
    -      return self.username
    +        return self.username
     
     
     @register(ModelUser)
     class UserAdmin(TortoiseModelAdmin):
         list_display = ("username", "is_superuser", "is_active")
         list_display_links = ("username",)
    -    list_filter = ("username", "is_superuser", "is_active",)
    -    search_fields = ("id", "username",)
    +    list_filter = (
    +        "username",
    +        "is_superuser",
    +        "is_active",
    +    )
    +    search_fields = (
    +        "id",
    +        "username",
    +    )
         fieldsets = (
             (None, {"fields": ("username", "hash_password")}),
             ("Permissions", {"fields": ("is_active", "is_superuser")}),
    @@ -513,7 +516,8 @@
             "username": (WidgetType.SlugInput, {"required": True}),
             "password": (WidgetType.PasswordInput, {"passwordModalForm": True}),
         }
    -    actions = TortoiseModelAdmin.actions + (
    +    actions = (
    +        *TortoiseModelAdmin.actions,
             "activate",
             "deactivate",
         )
    @@ -526,10 +530,10 @@
                 return None
             return user.id
     
    -    async def change_password(self, user_id: int, password: str) -> None:
    -        user = await self.model_cls.filter(id=user_id).first()
    +    async def change_password(self, id: UUID | int, password: str) -> None:
    +        user = await self.model_cls.filter(id=id).first()
             if not user:
    -            return None
    +            return
             user.hash_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
             await user.save(update_fields=("hash_password",))
     
    @@ -551,7 +555,7 @@
         model_name_prefix: str | None = None
     
         # A list of actions to make available on the change list page.
    -    # You have to implement methods with names like action_name in your ModelAdmin class and decorate them with @action decorator.  # noqa: E501
    +    # You have to implement methods with names like action_name in your ModelAdmin class and decorate them with @action decorator.
         # Example of usage:
         #
         # actions = ("make_published",)
    @@ -563,23 +567,23 @@
         actions: Sequence[str] = ()
     
         # Controls where on the page the actions bar appears.
    -    # By default, the admin changelist displays actions at the top of the page (actions_on_top = False; actions_on_bottom = True).  # noqa: E501
    +    # By default, the admin changelist displays actions at the top of the page (actions_on_top = False; actions_on_bottom = True).
         # Example of usage: actions_on_top = True
         actions_on_top: bool = False
     
         # Controls where on the page the actions bar appears.
    -    # By default, the admin changelist displays actions at the top of the page (actions_on_top = False; actions_on_bottom = True).  # noqa: E501
    +    # By default, the admin changelist displays actions at the top of the page (actions_on_top = False; actions_on_bottom = True).
         # Example of usage: actions_on_bottom = False
         actions_on_bottom: bool = True
     
    -    # Controls whether a selection counter is displayed next to the action dropdown. By default, the admin changelist will display it  # noqa: E501
    +    # Controls whether a selection counter is displayed next to the action dropdown. By default, the admin changelist will display it
         # Example of usage: actions_selection_counter = False
         actions_selection_counter: bool = True
     
         # Not supported setting
         # date_hierarchy
     
    -    # This attribute overrides the default display value for record’s fields that are empty (None, empty string, etc.). The default value is - (a dash).  # noqa: E501
    +    # This attribute overrides the default display value for record's fields that are empty (None, empty string, etc.). The default value is - (a dash).
         # Example of usage: empty_value_display = "N/A"
         empty_value_display: str = "-"
     
    @@ -594,17 +598,17 @@
         fields: Sequence[str] = ()
     
         # Set fieldsets to control the layout of admin “add” and “change” pages.
    -    # fieldsets is a list of two-tuples, in which each two-tuple represents a fieldset on the admin form page. (A fieldset is a “section” of the form.)  # noqa: E501
    +    # fieldsets is a list of two-tuples, in which each two-tuple represents a fieldset on the admin form page. (A fieldset is a “section” of the form.)
         fieldsets: Sequence[tuple[str | None, dict[str, Sequence[str]]]] = ()
     
         # By default, a ManyToManyField is displayed in the admin dashboard with a select multiple.
         # However, multiple-select boxes can be difficult to use when selecting many items.
    -    # Adding a ManyToManyField to this list will instead use a nifty unobtrusive JavaScript “filter” interface that allows searching within the options.  # noqa: E501
    -    # The unselected and selected options appear in two boxes side by side. See filter_vertical to use a vertical interface.  # noqa: E501
    +    # Adding a ManyToManyField to this list will instead use a nifty unobtrusive JavaScript “filter” interface that allows searching within the options.
    +    # The unselected and selected options appear in two boxes side by side. See filter_vertical to use a vertical interface.
         # Example of usage: filter_horizontal = ("groups", "user_permissions")
         filter_horizontal: Sequence[str] = ()
     
    -    # Same as filter_horizontal, but uses a vertical display of the filter interface with the box of unselected options appearing above the box of selected options.  # noqa: E501
    +    # Same as filter_horizontal, but uses a vertical display of the filter interface with the box of unselected options appearing above the box of selected options.
         # Example of usage: filter_vertical = ("groups", "user_permissions")
         filter_vertical: Sequence[str] = ()
     
    @@ -621,11 +625,11 @@
         formfield_overrides: dict[str, tuple[WidgetType, dict]] = {}
     
         # Set list_display to control which fields are displayed on the list page of the admin.
    -    # If you don’t set list_display, the admin site will display a single column that displays the __str__() representation of each object  # noqa: E501
    +    # If you don't set list_display, the admin site will display a single column that displays the __str__() representation of each object
         # Example of usage: list_display = ("id", "mobile_number", "email", "is_superuser", "is_active", "created_at")
         list_display: Sequence[str] = ()
     
    -    # Use list_display_links to control if and which fields in list_display should be linked to the “change” page for an object.  # noqa: E501
    +    # Use list_display_links to control if and which fields in list_display should be linked to the “change” page for an object.
         # Example of usage: list_display_links = ("id", "mobile_number", "email")
         list_display_links: Sequence[str] = ()
     
    @@ -647,21 +651,21 @@
         preserve_filters: bool = True
     
         # Set list_max_show_all to control how many items can appear on a “Show all” admin change list page.
    -    # The admin will display a “Show all” link on the change list only if the total result count is less than or equal to this setting. By default, this is set to 200.  # noqa: E501
    +    # The admin will display a “Show all” link on the change list only if the total result count is less than or equal to this setting. By default, this is set to 200.
         # Example of usage: list_max_show_all = 100
         list_max_show_all: int = 200
     
    -    # Set list_per_page to control how many items appear on each paginated admin list page. By default, this is set to 10.  # noqa: E501
    +    # Set list_per_page to control how many items appear on each paginated admin list page. By default, this is set to 10.
         # Example of usage: list_per_page = 50
         list_per_page = 10
     
    -    # Set list_select_related to tell ORM to use select_related() in retrieving the list of objects on the admin list page.  # noqa: E501
    +    # Set list_select_related to tell ORM to use select_related() in retrieving the list of objects on the admin list page.
         # This can save you a bunch of database queries.
         # Example of usage: list_select_related = ("user",)
         list_select_related: Sequence[str] = ()
     
         # Set ordering to specify how lists of objects should be ordered in the admin views.
    -    # This should be a list or tuple in the same format as a model’s ordering parameter.
    +    # This should be a list or tuple in the same format as a model's ordering parameter.
         # Example of usage: ordering = ("-created_at",)
         ordering: Sequence[str] = ()
     
    @@ -676,7 +680,7 @@
         # (e.g. substituting dashes for spaces and lowercasing ASCII letters).
         # prepopulated_fields: dict[str, Sequence[str]] = {}
     
    -    # By default, FastAPI admin uses a select-box interface (select) for fields that are ForeignKey or have choices set.  # noqa: E501
    +    # By default, FastAPI admin uses a select-box interface (select) for fields that are ForeignKey or have choices set.
         # If a field is present in radio_fields, FastAPI admin will use a radio-button interface instead.
         # Example of usage: radio_fields = ("user",)
         radio_fields: Sequence[str] = ()
    @@ -685,8 +689,8 @@
         # autocomplete_fields
     
         # By default, FastAPI admin uses a select-box interface (select) for fields that are ForeignKey.
    -    # Sometimes you don’t want to incur the overhead of having to select all the related instances to display in the drop-down.  # noqa: E501
    -    # raw_id_fields is a list of fields you would like to change into an Input widget for either a ForeignKey or ManyToManyField.  # noqa: E501
    +    # Sometimes you don't want to incur the overhead of having to select all the related instances to display in the drop-down.
    +    # raw_id_fields is a list of fields you would like to change into an Input widget for either a ForeignKey or ManyToManyField.
         # Example of usage: raw_id_fields = ("user",)
         raw_id_fields: Sequence[str] = ()
     
    @@ -696,7 +700,7 @@
         readonly_fields: Sequence[str] = ()
     
         # Set search_fields to enable a search box on the admin list page.
    -    # This should be set to a list of field names that will be searched whenever somebody submits a search query in that text box.  # noqa: E501
    +    # This should be set to a list of field names that will be searched whenever somebody submits a search query in that text box.
         # Example of usage: search_fields = ("mobile_number", "email")
         search_fields: Sequence[str] = ()
     
    @@ -717,6 +721,12 @@
         # Example of usage: sortable_by = ("mobile_number", "email")
         sortable_by: Sequence[str] = ()
     
    +    # An override to the verbose_name from the model's inner Meta class.
    +    verbose_name: str | None = None
    +
    +    # An override to the verbose_name_plural from the model's inner Meta class.
    +    verbose_name_plural: str | None = None
    +
         def __init__(self, model_cls: Any):
             """This method is used to initialize admin class.
     
    @@ -827,7 +837,7 @@
     
             :return: A db session maker.
             """
    -        return getattr(cls, "db_session_maker")
    +        return cls.db_session_maker
     
         @classmethod
         def set_sessionmaker(cls, db_session_maker: Any) -> None:
    @@ -836,7 +846,7 @@
             :params db_session: a db session maker.
             :return: None.
             """
    -        setattr(cls, "db_session_maker", db_session_maker)
    +        cls.db_session_maker = db_session_maker
     
         def get_fields_for_serialize(self) -> set[str]:
             """This method is used to get fields for serialize.
    @@ -1005,27 +1015,33 @@
             fields = self.get_model_fields_with_widget_types(with_m2m=False)
     
             export_fields = [f.name for f in fields]
    -        output = StringIO()
    -        if not export_format or export_format == ExportFormat.CSV:
    -            writer = csv.DictWriter(output, fieldnames=export_fields)
    -            writer.writeheader()
    -            for obj in objs:
    -                obj_dict = await self.serialize_obj(obj, list_view=True)
    -                obj_dict = {k: v for k, v in obj_dict.items() if k in export_fields}
    -                writer.writerow(obj_dict)
    -        if not export_format or export_format == ExportFormat.JSON:
    -
    -            class JSONEncoder(json.JSONEncoder):
    -                def default(self, obj):
    -                    try:
    -                        return super().default(obj)
    -                    except TypeError:
    -                        return str(obj)
    -
    -            json.dump([await self.serialize_obj(obj, list_view=True) for obj in objs], output, cls=JSONEncoder)
    -
    -        output.seek(0)
    -        return output
    +
    +        match export_format:
    +            case ExportFormat.CSV:
    +                output = StringIO()
    +                writer = csv.DictWriter(output, fieldnames=export_fields)
    +                writer.writeheader()
    +                for obj in objs:
    +                    obj_dict = await self.serialize_obj(obj, list_view=True)
    +                    obj_dict = {k: v for k, v in obj_dict.items() if k in export_fields}
    +                    writer.writerow(obj_dict)
    +                output.seek(0)
    +                return output
    +            case ExportFormat.JSON:
    +
    +                class JSONEncoder(json.JSONEncoder):
    +                    def default(self, obj):
    +                        try:
    +                            return super().default(obj)
    +                        except TypeError:
    +                            return str(obj)
    +
    +                output = StringIO()
    +                json.dump([await self.serialize_obj(obj, list_view=True) for obj in objs], output, cls=JSONEncoder)
    +                output.seek(0)
    +                return output
    +            case _:
    +                return None
     
         def has_add_permission(self, user_id: UUID | int | None = None) -> bool:
             """This method is used to check if user has permission to add new model instance.
    @@ -1071,7 +1087,7 @@
         # Example of usage: save_as = True
         save_as: bool = False
     
    -    # When save_as_continue=True, the default redirect after saving the new object is to the change view for that object.  # noqa: E501
    +    # When save_as_continue=True, the default redirect after saving the new object is to the change view for that object.
         # If you set save_as_continue=False, the redirect will be to the changelist view.
         # Example of usage: save_as_continue = False
         save_as_continue: bool = False
    @@ -1138,11 +1154,13 @@
       
     

    Note: Please see antd components for more details (e.g. to see how they look like).


    Inline Model Admins

    Registering Inlines

       
    -from tortoise.models import Model
    -from tortoise import fields
    +from uuid import UUID
    +
     import bcrypt
    +from tortoise import fields
    +from tortoise.models import Model
     
    -from fastadmin import register, TortoiseModelAdmin, action, WidgetType, TortoiseInlineModelAdmin
    +from fastadmin import TortoiseInlineModelAdmin, TortoiseModelAdmin, WidgetType, action, register
     
     
     class InlineUser(Model):
    @@ -1152,7 +1170,7 @@
         is_active = fields.BooleanField(default=False)
     
         def __str__(self):
    -      return self.username
    +        return self.username
     
     
     class InlineUserMessage(Model):
    @@ -1160,7 +1178,7 @@
         message = fields.TextField()
     
         def __str__(self):
    -      return self.message
    +        return self.message
     
     
     class UserMessageAdminInline(TortoiseInlineModelAdmin):
    @@ -1175,8 +1193,15 @@
     class UserAdmin(TortoiseModelAdmin):
         list_display = ("username", "is_superuser", "is_active")
         list_display_links = ("username",)
    -    list_filter = ("username", "is_superuser", "is_active",)
    -    search_fields = ("id", "username",)
    +    list_filter = (
    +        "username",
    +        "is_superuser",
    +        "is_active",
    +    )
    +    search_fields = (
    +        "id",
    +        "username",
    +    )
         fieldsets = (
             (None, {"fields": ("username", "hash_password")}),
             ("Permissions", {"fields": ("is_active", "is_superuser")}),
    @@ -1185,7 +1210,8 @@
             "username": (WidgetType.SlugInput, {"required": True}),
             "password": (WidgetType.PasswordInput, {"passwordModalForm": True}),
         }
    -    actions = TortoiseModelAdmin.actions + (
    +    actions = (
    +        *TortoiseModelAdmin.actions,
             "activate",
             "deactivate",
         )
    @@ -1200,10 +1226,10 @@
                 return None
             return user.id
     
    -    async def change_password(self, user_id: int, password: str) -> None:
    -        user = await self.model_cls.filter(id=user_id).first()
    +    async def change_password(self, id: UUID | int, password: str) -> None:
    +        user = await self.model_cls.filter(id=id).first()
             if not user:
    -            return None
    +            return
             user.hash_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
             await user.save(update_fields=("hash_password",))
     
    @@ -1230,21 +1256,15 @@
         fk_name: str | None = None
     
         # This controls the maximum number of forms to show in the inline.
    -    # This doesn’t directly correlate to the number of objects, but can if the value is small enough.
    +    # This doesn't directly correlate to the number of objects, but can if the value is small enough.
         # See Limiting the number of editable objects for more information.
         max_num: int = 10
     
         # This controls the minimum number of forms to show in the inline.
         min_num: int = 1
     
    -    # An override to the verbose_name from the model’s inner Meta class.
    -    verbose_name: str | None = None
    -
    -    # An override to the verbose_name_plural from the model’s inner Meta class.
    -    verbose_name_plural: str | None = None
    -
       
    -

    Changelog

    See what's new added, changed, fixed, improved or updated in the latest versions.

    v0.1.37

    Bug fixes.

    v0.1.36

    Added autogeneration of documentation and examples.

    v0.1.35

    Added DashboardWidgetAdmin class and charts for dashboard.

    v0.1.34

    Added SlugInput, EmailInput, PhoneInput, UrlInput, JsonTextArea widget types.

    v0.1.33

    Added list_display_widths parameter.

    v0.1.32

    Added Upload widget type.

    v0.1.31

    Added PasswordInput widget type.