oldman 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oldman/__init__.py +14 -0
- oldman/apps/__init__.py +23 -0
- oldman/apps/admin/__init__.py +15 -0
- oldman/apps/admin/apps.py +19 -0
- oldman/apps/admin/commands.py +94 -0
- oldman/apps/admin/crud.py +94 -0
- oldman/apps/admin/forms.py +323 -0
- oldman/apps/admin/locales/zh_Hans/LC_MESSAGES/messages.mo +0 -0
- oldman/apps/admin/locales/zh_Hant/LC_MESSAGES/messages.mo +0 -0
- oldman/apps/admin/model_admin.py +524 -0
- oldman/apps/admin/permissions.py +42 -0
- oldman/apps/admin/runtime.py +121 -0
- oldman/apps/admin/settings.py +17 -0
- oldman/apps/admin/site.py +1345 -0
- oldman/apps/admin/static/oldman/admin/.vite/manifest.json +212 -0
- oldman/apps/admin/static/oldman/admin/assets/autocomplete-jPdFW4Z7.js +1 -0
- oldman/apps/admin/static/oldman/admin/assets/date-time-picker-DXKOYDM_.js +5 -0
- oldman/apps/admin/static/oldman/admin/assets/date-time-picker-XdZfF2Hc.css +1 -0
- oldman/apps/admin/static/oldman/admin/assets/dm-sans-latin-300-normal-C46oWILc.woff2 +0 -0
- oldman/apps/admin/static/oldman/admin/assets/dm-sans-latin-300-normal-CsNfKS1n.woff +0 -0
- oldman/apps/admin/static/oldman/admin/assets/dm-sans-latin-400-normal-BwCSEQnW.woff +0 -0
- oldman/apps/admin/static/oldman/admin/assets/dm-sans-latin-400-normal-CW0RaeGs.woff2 +0 -0
- oldman/apps/admin/static/oldman/admin/assets/dm-sans-latin-500-normal-B9HHJjqV.woff2 +0 -0
- oldman/apps/admin/static/oldman/admin/assets/dm-sans-latin-500-normal-Dr3UlScf.woff +0 -0
- oldman/apps/admin/static/oldman/admin/assets/dm-sans-latin-600-normal-Aqo67rzb.woff2 +0 -0
- oldman/apps/admin/static/oldman/admin/assets/dm-sans-latin-600-normal-BmdmIIQ2.woff +0 -0
- oldman/apps/admin/static/oldman/admin/assets/dm-sans-latin-700-normal-CUSSCpQX.woff +0 -0
- oldman/apps/admin/static/oldman/admin/assets/dm-sans-latin-700-normal-DvUfVpUG.woff2 +0 -0
- oldman/apps/admin/static/oldman/admin/assets/dropdown-DQmEUd1r.js +1 -0
- oldman/apps/admin/static/oldman/admin/assets/form-DZC6BBLO.js +1 -0
- oldman/apps/admin/static/oldman/admin/assets/form-validator-Gq06sYfE.js +1 -0
- oldman/apps/admin/static/oldman/admin/assets/language-switcher-CjE-89Lp.js +1 -0
- oldman/apps/admin/static/oldman/admin/assets/main-BVK9XJmx.css +7 -0
- oldman/apps/admin/static/oldman/admin/assets/main-BcU_G9Qv.js +116 -0
- oldman/apps/admin/static/oldman/admin/assets/modal-C9DpqkgN.js +1 -0
- oldman/apps/admin/static/oldman/admin/assets/preloader-C4Cw6JCy.js +1 -0
- oldman/apps/admin/static/oldman/admin/assets/select-79I-Au9m.js +1 -0
- oldman/apps/admin/static/oldman/admin/assets/select-BnyRxOjF.css +1 -0
- oldman/apps/admin/static/oldman/admin/assets/sidebar-menu-DqAbkKBH.js +1 -0
- oldman/apps/admin/static/oldman/admin/assets/table-B0PQSRm0.js +1 -0
- oldman/apps/admin/static/oldman/admin/assets/table-filter-form-zfFJ-D_E.js +1 -0
- oldman/apps/admin/static/oldman/admin/assets/table-json-BQldyVm2.js +1 -0
- oldman/apps/admin/static/oldman/admin/assets/turbo.es2017-esm-C9IpioHW.js +29 -0
- oldman/apps/admin/staticfiles.py +75 -0
- oldman/apps/admin/table.py +194 -0
- oldman/apps/admin/template.py +26 -0
- oldman/apps/admin/templates/.gitkeep +1 -0
- oldman/apps/admin/templates/admin/base.html +82 -0
- oldman/apps/admin/templates/admin/index.html +32 -0
- oldman/apps/admin/templates/admin/login.html +60 -0
- oldman/apps/admin/templates/admin/model/confirm_delete.html +27 -0
- oldman/apps/admin/templates/admin/model/delete_modal_form.html +18 -0
- oldman/apps/admin/templates/admin/model/form.html +35 -0
- oldman/apps/admin/templates/admin/model/list.html +71 -0
- oldman/apps/admin/templates/admin/model/status_modal_form.html +19 -0
- oldman/apps/admin/templates/admin/partials/language_switcher.html +5 -0
- oldman/apps/admin/templates/admin/partials/topbar.html +21 -0
- oldman/apps/admin/templates/admin/user_notifications.html +12 -0
- oldman/apps/admin/templates/admin/user_session.html +7 -0
- oldman/apps/admin/users.py +56 -0
- oldman/apps/config.py +121 -0
- oldman/apps/registry.py +490 -0
- oldman/auth/__init__.py +34 -0
- oldman/auth/apps.py +19 -0
- oldman/auth/base.py +138 -0
- oldman/auth/contracts.py +244 -0
- oldman/auth/migrations/__init__.py +0 -0
- oldman/auth/migrations/b7c396a46832_create_oldman_user.py +80 -0
- oldman/auth/models.py +10 -0
- oldman/auth/registry.py +44 -0
- oldman/auth/security.py +42 -0
- oldman/auth/services.py +224 -0
- oldman/auth/settings.py +28 -0
- oldman/cache/__init__.py +17 -0
- oldman/cache/backends/__init__.py +4 -0
- oldman/cache/backends/memory.py +120 -0
- oldman/cache/backends/redis.py +263 -0
- oldman/cache/base.py +367 -0
- oldman/cache/exceptions.py +34 -0
- oldman/cache/images.py +420 -0
- oldman/cache/two_level.py +55 -0
- oldman/cache/utils.py +270 -0
- oldman/cli/__init__.py +32 -0
- oldman/cli/__main__.py +6 -0
- oldman/cli/_main.py +1038 -0
- oldman/cli/_service_discovery.py +14 -0
- oldman/cli/babel.cfg +5 -0
- oldman/cli/commands.py +68 -0
- oldman/cli/database.py +257 -0
- oldman/cli/discovery.py +5 -0
- oldman/cli/fixtures.py +60 -0
- oldman/cli/i18n_commands.py +49 -0
- oldman/cli/locales/README.md +29 -0
- oldman/cli/locales/zh_Hans/LC_MESSAGES/messages.mo +0 -0
- oldman/cli/locales/zh_Hant/LC_MESSAGES/messages.mo +0 -0
- oldman/cli/localization.py +302 -0
- oldman/cli/scaffold.py +331 -0
- oldman/cli/service.py +198 -0
- oldman/cli/settings.py +70 -0
- oldman/cli/shell.py +37 -0
- oldman/cli/staticfiles.py +59 -0
- oldman/compat/__init__.py +7 -0
- oldman/compat/django/__init__.py +7 -0
- oldman/compat/django/cache.py +108 -0
- oldman/compat/django/serializers.py +34 -0
- oldman/conf/__init__.py +79 -0
- oldman/conf/base.py +156 -0
- oldman/conf/constants.py +157 -0
- oldman/conf/containers.py +275 -0
- oldman/conf/manager.py +574 -0
- oldman/conf/schemas.py +866 -0
- oldman/contrib/__init__.py +7 -0
- oldman/contrib/http/__init__.py +37 -0
- oldman/contrib/http/_logging.py +43 -0
- oldman/contrib/http/backends/__init__.py +14 -0
- oldman/contrib/http/backends/aiohttp.py +356 -0
- oldman/contrib/http/backends/curl.py +455 -0
- oldman/contrib/http/backends/httpx.py +199 -0
- oldman/contrib/http/base.py +91 -0
- oldman/contrib/http/contants.py +50 -0
- oldman/contrib/http/exceptions.py +63 -0
- oldman/contrib/http/multi_client.py +554 -0
- oldman/contrib/http/reconnect_stream.py +559 -0
- oldman/contrib/http/response.py +702 -0
- oldman/contrib/http/schemas.py +29 -0
- oldman/contrib/proxy/__init__.py +19 -0
- oldman/contrib/proxy/base.py +1177 -0
- oldman/contrib/proxy/encrypt.py +568 -0
- oldman/contrib/proxy/simple.py +724 -0
- oldman/db/__init__.py +31 -0
- oldman/db/fixtures.py +584 -0
- oldman/db/migrations/__init__.py +65 -0
- oldman/db/migrations/alembic.py +287 -0
- oldman/db/migrations/autogenerate.py +1505 -0
- oldman/db/migrations/commands.py +1070 -0
- oldman/db/migrations/interaction.py +120 -0
- oldman/db/migrations/metadata.py +196 -0
- oldman/db/migrations/project.py +296 -0
- oldman/db/migrations/revisions.py +652 -0
- oldman/db/migrations/state.py +410 -0
- oldman/db/migrations/templates/env.py +67 -0
- oldman/db/migrations/templates/script.py.mako +27 -0
- oldman/db/models.py +250 -0
- oldman/db/schemas.py +236 -0
- oldman/db/services.py +5 -0
- oldman/db/session.py +365 -0
- oldman/db/sqlalchemy/__init__.py +7 -0
- oldman/db/sqlalchemy/cache.py +469 -0
- oldman/db/sqlalchemy/log.py +134 -0
- oldman/db/sqlalchemy/models.py +246 -0
- oldman/db/sqlalchemy/services.py +18 -0
- oldman/db/sqlalchemy/session.py +22 -0
- oldman/db/sqlalchemy/utils.py +64 -0
- oldman/i18n/__init__.py +46 -0
- oldman/i18n/catalogs.py +47 -0
- oldman/i18n/commands.py +205 -0
- oldman/i18n/data/oldman_web_messages.json +639 -0
- oldman/i18n/framework-babel.cfg +24 -0
- oldman/i18n/frontend.py +398 -0
- oldman/i18n/profiles.py +35 -0
- oldman/i18n/registry.py +183 -0
- oldman/i18n/serialization.py +165 -0
- oldman/i18n/translations.py +109 -0
- oldman/i18n/utils.py +55 -0
- oldman/licenses/aiocache-BSD-3-Clause.txt +12 -0
- oldman/licenses/flag-icons-LICENSE.txt +26 -0
- oldman/licenses/nats-py-Apache-2.0.txt +201 -0
- oldman/logging/__init__.py +25 -0
- oldman/logging/config.py +196 -0
- oldman/logging/formatters.py +216 -0
- oldman/logging/handlers.py +195 -0
- oldman/logging/rotation.py +229 -0
- oldman/logging/runtime.py +402 -0
- oldman/processes/__init__.py +28 -0
- oldman/processes/_subprocess_supervisor.py +262 -0
- oldman/processes/executor.py +602 -0
- oldman/processes/subprocess.py +568 -0
- oldman/providers/__init__.py +7 -0
- oldman/providers/nats/__init__.py +13 -0
- oldman/providers/nats/_compat.py +197 -0
- oldman/providers/nats/bus.py +32 -0
- oldman/providers/nats/config.py +45 -0
- oldman/providers/nats/connection.py +524 -0
- oldman/providers/nats/serializers.py +195 -0
- oldman/providers/redis/__init__.py +17 -0
- oldman/providers/redis/client.py +206 -0
- oldman/providers/redis/redis.py +266 -0
- oldman/py.typed +1 -0
- oldman/runtime/__init__.py +81 -0
- oldman/runtime/_taskiq_process.py +166 -0
- oldman/runtime/base.py +605 -0
- oldman/runtime/bootstrap.py +91 -0
- oldman/runtime/discovery.py +212 -0
- oldman/runtime/simple.py +218 -0
- oldman/runtime/taskiq.py +316 -0
- oldman/runtime/web.py +421 -0
- oldman/scaffolds/__init__.py +1 -0
- oldman/scaffolds/app/api_app/.gitkeep +1 -0
- oldman/scaffolds/app/api_app/apps/__app_slug__/__init__.py.tpl +1 -0
- oldman/scaffolds/app/api_app/apps/__app_slug__/apps.py.tpl +16 -0
- oldman/scaffolds/app/api_app/apps/__app_slug__/migrations/__init__.py.tpl +1 -0
- oldman/scaffolds/app/api_app/apps/__app_slug__/models.py.tpl +1 -0
- oldman/scaffolds/app/api_app/apps/__app_slug__/views.py.tpl +44 -0
- oldman/scaffolds/app/dashboard_app/.gitkeep +1 -0
- oldman/scaffolds/app/dashboard_app/apps/__app_slug__/__init__.py.tpl +1 -0
- oldman/scaffolds/app/dashboard_app/apps/__app_slug__/apps.py.tpl +16 -0
- oldman/scaffolds/app/dashboard_app/apps/__app_slug__/migrations/__init__.py.tpl +1 -0
- oldman/scaffolds/app/dashboard_app/apps/__app_slug__/models.py.tpl +1 -0
- oldman/scaffolds/app/dashboard_app/apps/__app_slug__/views.py.tpl +36 -0
- oldman/scaffolds/app/dashboard_app/frontend/src/pages/__app_slug__.ts.tpl +13 -0
- oldman/scaffolds/app/dashboard_app/templates/__app_slug__/index.html.tpl +12 -0
- oldman/scaffolds/app/service_app/.gitkeep +1 -0
- oldman/scaffolds/app/service_app/apps/__app_slug__/__init__.py.tpl +1 -0
- oldman/scaffolds/app/service_app/apps/__app_slug__/apps.py.tpl +16 -0
- oldman/scaffolds/app/service_app/apps/__app_slug__/migrations/__init__.py.tpl +1 -0
- oldman/scaffolds/app/service_app/apps/__app_slug__/services.py.tpl +1 -0
- oldman/scaffolds/app/web_app/.gitkeep +1 -0
- oldman/scaffolds/app/web_app/apps/__app_slug__/__init__.py.tpl +1 -0
- oldman/scaffolds/app/web_app/apps/__app_slug__/apps.py.tpl +16 -0
- oldman/scaffolds/app/web_app/apps/__app_slug__/migrations/__init__.py.tpl +1 -0
- oldman/scaffolds/app/web_app/apps/__app_slug__/models.py.tpl +1 -0
- oldman/scaffolds/app/web_app/apps/__app_slug__/views.py.tpl +13 -0
- oldman/scaffolds/app/web_app/templates/__app_slug__/index.html.tpl +7 -0
- oldman/scaffolds/project/api_service/.gitignore.tpl +4 -0
- oldman/scaffolds/project/api_service/.gitkeep +1 -0
- oldman/scaffolds/project/api_service/README.md.tpl +47 -0
- oldman/scaffolds/project/api_service/apps/__init__.py.tpl +1 -0
- oldman/scaffolds/project/api_service/babel.cfg.tpl +28 -0
- oldman/scaffolds/project/api_service/config/__init__.py.tpl +1 -0
- oldman/scaffolds/project/api_service/config/schemas.py.tpl +9 -0
- oldman/scaffolds/project/api_service/config/settings.py.tpl +10 -0
- oldman/scaffolds/project/api_service/data/{{ service_name }}_settings.yaml.tpl +3 -0
- oldman/scaffolds/project/api_service/locales/.gitkeep +1 -0
- oldman/scaffolds/project/api_service/pyproject.toml.tpl +14 -0
- oldman/scaffolds/project/api_service/run.sh.tpl +14 -0
- oldman/scaffolds/project/api_service/services/__init__.py.tpl +1 -0
- oldman/scaffolds/project/api_service/services/{{ service_name }}.py.tpl +28 -0
- oldman/scaffolds/project/app_service/.gitignore.tpl +4 -0
- oldman/scaffolds/project/app_service/.gitkeep +1 -0
- oldman/scaffolds/project/app_service/README.md.tpl +30 -0
- oldman/scaffolds/project/app_service/apps/__init__.py.tpl +1 -0
- oldman/scaffolds/project/app_service/config/__init__.py.tpl +1 -0
- oldman/scaffolds/project/app_service/config/schemas.py.tpl +9 -0
- oldman/scaffolds/project/app_service/config/settings.py.tpl +10 -0
- oldman/scaffolds/project/app_service/data/{{ service_name }}_settings.yaml.tpl +3 -0
- oldman/scaffolds/project/app_service/pyproject.toml.tpl +14 -0
- oldman/scaffolds/project/app_service/run.sh.tpl +14 -0
- oldman/scaffolds/project/app_service/services/__init__.py.tpl +1 -0
- oldman/scaffolds/project/app_service/services/{{ service_name }}.py.tpl +18 -0
- oldman/scaffolds/project/cli_app/.gitkeep +1 -0
- oldman/scaffolds/project/cli_app/README.md.tpl +16 -0
- oldman/scaffolds/project/cli_app/main.py.tpl +13 -0
- oldman/scaffolds/project/cli_app/pyproject.toml.tpl +14 -0
- oldman/scaffolds/project/dashboard/.gitignore.tpl +7 -0
- oldman/scaffolds/project/dashboard/.gitkeep +1 -0
- oldman/scaffolds/project/dashboard/README.md.tpl +100 -0
- oldman/scaffolds/project/dashboard/apps/__init__.py.tpl +1 -0
- oldman/scaffolds/project/dashboard/babel.cfg.tpl +28 -0
- oldman/scaffolds/project/dashboard/config/__init__.py.tpl +1 -0
- oldman/scaffolds/project/dashboard/config/schemas.py.tpl +9 -0
- oldman/scaffolds/project/dashboard/config/settings.py.tpl +10 -0
- oldman/scaffolds/project/dashboard/data/{{ service_name }}_settings.yaml.tpl +3 -0
- oldman/scaffolds/project/dashboard/frontend/index.html.tpl +2 -0
- oldman/scaffolds/project/dashboard/frontend/package.json.tpl +26 -0
- oldman/scaffolds/project/dashboard/frontend/src/app.css.tpl +13 -0
- oldman/scaffolds/project/dashboard/frontend/src/generated/icons.css.tpl +18 -0
- oldman/scaffolds/project/dashboard/frontend/src/main.ts.tpl +89 -0
- oldman/scaffolds/project/dashboard/frontend/tsconfig.json.tpl +11 -0
- oldman/scaffolds/project/dashboard/frontend/vite.config.ts.tpl +20 -0
- oldman/scaffolds/project/dashboard/locales/.gitkeep +1 -0
- oldman/scaffolds/project/dashboard/pyproject.toml.tpl +14 -0
- oldman/scaffolds/project/dashboard/run.sh.tpl +14 -0
- oldman/scaffolds/project/dashboard/scripts/build_frontend_i18n.py.tpl +119 -0
- oldman/scaffolds/project/dashboard/services/__init__.py.tpl +1 -0
- oldman/scaffolds/project/dashboard/services/{{ service_name }}.py.tpl +125 -0
- oldman/scaffolds/project/dashboard/static/.oldman_keep.tpl +1 -0
- oldman/scaffolds/project/dashboard/templates/base.html.tpl +44 -0
- oldman/scaffolds/project/dashboard/templates/errors/403.html.tpl +1 -0
- oldman/scaffolds/project/dashboard/templates/errors/404.html.tpl +1 -0
- oldman/scaffolds/project/dashboard/templates/errors/500.html.tpl +1 -0
- oldman/scaffolds/project/dashboard/templates/errors/default.html.tpl +1 -0
- oldman/scaffolds/project/web_service/.gitignore.tpl +5 -0
- oldman/scaffolds/project/web_service/.gitkeep +1 -0
- oldman/scaffolds/project/web_service/README.md.tpl +40 -0
- oldman/scaffolds/project/web_service/apps/__init__.py.tpl +1 -0
- oldman/scaffolds/project/web_service/babel.cfg.tpl +28 -0
- oldman/scaffolds/project/web_service/config/__init__.py.tpl +1 -0
- oldman/scaffolds/project/web_service/config/schemas.py.tpl +9 -0
- oldman/scaffolds/project/web_service/config/settings.py.tpl +10 -0
- oldman/scaffolds/project/web_service/data/{{ service_name }}_settings.yaml.tpl +3 -0
- oldman/scaffolds/project/web_service/locales/.gitkeep +1 -0
- oldman/scaffolds/project/web_service/pyproject.toml.tpl +14 -0
- oldman/scaffolds/project/web_service/run.sh.tpl +14 -0
- oldman/scaffolds/project/web_service/services/__init__.py.tpl +1 -0
- oldman/scaffolds/project/web_service/services/{{ service_name }}.py.tpl +39 -0
- oldman/scaffolds/project/web_service/static/.oldman_keep.tpl +1 -0
- oldman/scaffolds/project/web_service/templates/base.html.tpl +11 -0
- oldman/scaffolds/service/simple/services/{{ service_name }}.py.tpl +18 -0
- oldman/scaffolds/service/taskiq_scheduler/services/{{ service_name }}.py.tpl +7 -0
- oldman/scaffolds/service/taskiq_worker/services/{{ service_name }}.py.tpl +7 -0
- oldman/scaffolds/service/web/services/{{ service_name }}.py.tpl +39 -0
- oldman/security/__init__.py +17 -0
- oldman/security/jwt.py +155 -0
- oldman/security/rate_limiter/__init__.py +8 -0
- oldman/security/rate_limiter/token_bucket.py +72 -0
- oldman/serializers/__init__.py +31 -0
- oldman/serializers/base.py +226 -0
- oldman/serializers/cache.py +115 -0
- oldman/storage/__init__.py +46 -0
- oldman/storage/_web.py +89 -0
- oldman/storage/backends/__init__.py +4 -0
- oldman/storage/backends/filesystem.py +540 -0
- oldman/storage/backends/memory.py +98 -0
- oldman/storage/base.py +166 -0
- oldman/storage/exceptions.py +38 -0
- oldman/storage/lifecycle.py +261 -0
- oldman/storage/models.py +106 -0
- oldman/storage/registry.py +198 -0
- oldman/storage/streams.py +50 -0
- oldman/tasks/__init__.py +23 -0
- oldman/tasks/base.py +52 -0
- oldman/tasks/communication.py +100 -0
- oldman/tasks/distributed/__init__.py +13 -0
- oldman/tasks/distributed/broker.py +353 -0
- oldman/tasks/distributed/middleware.py +40 -0
- oldman/tasks/distributed/receiver.py +26 -0
- oldman/tasks/distributed/results.py +67 -0
- oldman/tasks/distributed/scheduler.py +116 -0
- oldman/tasks/distributed/worker.py +144 -0
- oldman/tasks/manager.py +367 -0
- oldman/tasks/messages.py +39 -0
- oldman/tasks/queue.py +94 -0
- oldman/tasks/simple.py +326 -0
- oldman/tasks/worker.py +159 -0
- oldman/utils/__init__.py +7 -0
- oldman/utils/collections_utils.py +233 -0
- oldman/utils/console.py +10 -0
- oldman/utils/date.py +86 -0
- oldman/utils/decorators.py +79 -0
- oldman/utils/files.py +38 -0
- oldman/utils/hash.py +192 -0
- oldman/utils/hash_ids.py +61 -0
- oldman/utils/headers.py +41 -0
- oldman/utils/http.py +113 -0
- oldman/utils/loop_utls.py +179 -0
- oldman/utils/singleton.py +160 -0
- oldman/utils/strings_utils.py +285 -0
- oldman/utils/thread.py +210 -0
- oldman/utils/timezone.py +59 -0
- oldman/version.py +10 -0
- oldman/web/__init__.py +54 -0
- oldman/web/api/__init__.py +27 -0
- oldman/web/api/actions.py +70 -0
- oldman/web/api/enums.py +40 -0
- oldman/web/api/responses.py +29 -0
- oldman/web/auth/__init__.py +35 -0
- oldman/web/auth/decorators.py +229 -0
- oldman/web/auth/forms.py +60 -0
- oldman/web/auth/session.py +40 -0
- oldman/web/auth/user_session.py +231 -0
- oldman/web/components/__init__.py +7 -0
- oldman/web/components/charts/__init__.py +21 -0
- oldman/web/components/charts/config.py +26 -0
- oldman/web/components/charts/renderers.py +50 -0
- oldman/web/components/charts/request.py +41 -0
- oldman/web/components/charts/results.py +49 -0
- oldman/web/components/charts/sqlalchemy.py +35 -0
- oldman/web/components/charts/views.py +133 -0
- oldman/web/components/forms/__init__.py +70 -0
- oldman/web/components/forms/base.py +493 -0
- oldman/web/components/forms/choices.py +54 -0
- oldman/web/components/forms/fields.py +395 -0
- oldman/web/components/forms/layouts.py +69 -0
- oldman/web/components/forms/models.py +367 -0
- oldman/web/components/forms/renderers.py +592 -0
- oldman/web/components/forms/tailwind.py +31 -0
- oldman/web/components/forms/widgets.py +391 -0
- oldman/web/components/selects/__init__.py +23 -0
- oldman/web/components/selects/choices.py +51 -0
- oldman/web/components/selects/providers.py +299 -0
- oldman/web/components/selects/registry.py +41 -0
- oldman/web/components/selects/signing.py +81 -0
- oldman/web/components/selects/views.py +76 -0
- oldman/web/components/tables/__init__.py +22 -0
- oldman/web/components/tables/columns.py +123 -0
- oldman/web/components/tables/legacy.py +207 -0
- oldman/web/components/tables/renderers.py +314 -0
- oldman/web/components/tables/request.py +34 -0
- oldman/web/components/tables/results.py +23 -0
- oldman/web/components/tables/views.py +659 -0
- oldman/web/errors.py +61 -0
- oldman/web/exceptions.py +5 -0
- oldman/web/html.py +53 -0
- oldman/web/http.py +154 -0
- oldman/web/i18n/__init__.py +15 -0
- oldman/web/i18n/assets.py +34 -0
- oldman/web/i18n/request.py +55 -0
- oldman/web/i18n/translation.py +223 -0
- oldman/web/messages/__init__.py +35 -0
- oldman/web/messages/_cookie_storage.py +396 -0
- oldman/web/messages/_template.py +42 -0
- oldman/web/messages/actions.py +20 -0
- oldman/web/messages/flash.py +173 -0
- oldman/web/messages/notifications/__init__.py +38 -0
- oldman/web/messages/notifications/apps.py +17 -0
- oldman/web/messages/notifications/locales/zh_Hans/LC_MESSAGES/messages.mo +0 -0
- oldman/web/messages/notifications/locales/zh_Hant/LC_MESSAGES/messages.mo +0 -0
- oldman/web/messages/notifications/migrations/4858aab957ee_create_oldman_notification.py +52 -0
- oldman/web/messages/notifications/migrations/__init__.py +0 -0
- oldman/web/messages/notifications/models.py +74 -0
- oldman/web/messages/notifications/payloads.py +122 -0
- oldman/web/messages/notifications/rendering.py +228 -0
- oldman/web/messages/notifications/runtime.py +242 -0
- oldman/web/messages/notifications/service.py +556 -0
- oldman/web/middlewares/__init__.py +5 -0
- oldman/web/middlewares/_legacy_i18n.py +7 -0
- oldman/web/middlewares/context.py +17 -0
- oldman/web/middlewares/i18n.py +80 -0
- oldman/web/middlewares/session.py +7 -0
- oldman/web/package_data.py +18 -0
- oldman/web/request.py +27 -0
- oldman/web/response.py +161 -0
- oldman/web/routing.py +69 -0
- oldman/web/security/__init__.py +27 -0
- oldman/web/security/csrf/__init__.py +12 -0
- oldman/web/security/csrf/csrf_extension.py +61 -0
- oldman/web/security/csrf/decorators.py +91 -0
- oldman/web/security/csrf/manager.py +243 -0
- oldman/web/security/decryptors.py +62 -0
- oldman/web/security/fingerprint.py +107 -0
- oldman/web/security/keys.py +52 -0
- oldman/web/security/rate_limiter/__init__.py +13 -0
- oldman/web/security/rate_limiter/base.py +13 -0
- oldman/web/security/rate_limiter/fingerprint.py +118 -0
- oldman/web/security/rate_limiter/fixed_window.py +42 -0
- oldman/web/security/rate_limiter/lua.py +123 -0
- oldman/web/session/__init__.py +182 -0
- oldman/web/session/base.py +693 -0
- oldman/web/sse/__init__.py +27 -0
- oldman/web/sse/connection.py +208 -0
- oldman/web/sse/events.py +75 -0
- oldman/web/sse/extension.py +438 -0
- oldman/web/sse/messages.py +39 -0
- oldman/web/sse/pool.py +83 -0
- oldman/web/sse/publisher.py +163 -0
- oldman/web/sse/stream.py +111 -0
- oldman/web/static/oldman/images/flags/cn.svg +11 -0
- oldman/web/static/oldman/images/flags/tw.svg +32 -0
- oldman/web/static/oldman/images/flags/us.svg +171 -0
- oldman/web/staticfiles/__init__.py +40 -0
- oldman/web/staticfiles/bundles.py +261 -0
- oldman/web/staticfiles/collector.py +655 -0
- oldman/web/staticfiles/finders.py +107 -0
- oldman/web/staticfiles/urls.py +47 -0
- oldman/web/template/__init__.py +143 -0
- oldman/web/template/_legacy_init.py +7 -0
- oldman/web/template/i18n_extension.py +149 -0
- oldman/web/template/render.py +38 -0
- oldman/web/templates/.gitkeep +1 -0
- oldman/web/templates/oldman/auth/partials/account_controls.html +28 -0
- oldman/web/templates/oldman/auth/partials/password_form.html +20 -0
- oldman/web/templates/oldman/auth/user_session_content.html +135 -0
- oldman/web/templates/oldman/charts/default/shell.html +8 -0
- oldman/web/templates/oldman/dashboard/base.html +41 -0
- oldman/web/templates/oldman/dashboard/components/modal.html +62 -0
- oldman/web/templates/oldman/dashboard/components/modal_fragment.html +14 -0
- oldman/web/templates/oldman/dashboard/errors/403.html +10 -0
- oldman/web/templates/oldman/dashboard/errors/404.html +10 -0
- oldman/web/templates/oldman/dashboard/errors/500.html +10 -0
- oldman/web/templates/oldman/dashboard/errors/_base.html +39 -0
- oldman/web/templates/oldman/dashboard/errors/default.html +10 -0
- oldman/web/templates/oldman/dashboard/partials/back_to_top.html +3 -0
- oldman/web/templates/oldman/dashboard/partials/language_switcher.html +40 -0
- oldman/web/templates/oldman/dashboard/partials/preloader.html +3 -0
- oldman/web/templates/oldman/dashboard/partials/preloader_critical_css.html +33 -0
- oldman/web/templates/oldman/dashboard/partials/shell.html +34 -0
- oldman/web/templates/oldman/dashboard/partials/topbar.html +131 -0
- oldman/web/templates/oldman/errors/403.html +10 -0
- oldman/web/templates/oldman/errors/404.html +10 -0
- oldman/web/templates/oldman/errors/500.html +10 -0
- oldman/web/templates/oldman/errors/_base.html +32 -0
- oldman/web/templates/oldman/errors/default.html +10 -0
- oldman/web/templates/oldman/forms/default/actions.html +5 -0
- oldman/web/templates/oldman/forms/default/boolean_field.html +2 -0
- oldman/web/templates/oldman/forms/default/field.html +6 -0
- oldman/web/templates/oldman/forms/default/field_errors.html +1 -0
- oldman/web/templates/oldman/forms/default/fields.html +3 -0
- oldman/web/templates/oldman/forms/default/form.html +9 -0
- oldman/web/templates/oldman/forms/default/help.html +1 -0
- oldman/web/templates/oldman/forms/default/json_list_field.html +42 -0
- oldman/web/templates/oldman/forms/default/message.html +1 -0
- oldman/web/templates/oldman/forms/default/multi_step.html +22 -0
- oldman/web/templates/oldman/forms/default/password_field.html +9 -0
- oldman/web/templates/oldman/forms/default/radio_field.html +8 -0
- oldman/web/templates/oldman/forms/default/status.html +1 -0
- oldman/web/templates/oldman/forms/default/widgets/ajax_autocomplete.html +7 -0
- oldman/web/templates/oldman/forms/default/widgets/ajax_select.html +5 -0
- oldman/web/templates/oldman/forms/default/widgets/color_picker.html +5 -0
- oldman/web/templates/oldman/forms/default/widgets/input_spinner.html +5 -0
- oldman/web/templates/oldman/forms/default/widgets/rich_text.html +4 -0
- oldman/web/templates/oldman/messages/flash_message.html +13 -0
- oldman/web/templates/oldman/messages/notifications/center_content.html +99 -0
- oldman/web/templates/oldman/messages/notifications/topbar_fragment.html +24 -0
- oldman/web/templates/oldman/tables/default/cell.html +1 -0
- oldman/web/templates/oldman/tables/default/error.html +1 -0
- oldman/web/templates/oldman/tables/default/fragment.html +39 -0
- oldman/web/templates/oldman/tables/default/head.html +19 -0
- oldman/web/templates/oldman/tables/default/pagination.html +11 -0
- oldman/web/templates/oldman/tables/default/row.html +6 -0
- oldman/web/templates/oldman/tables/default/shell.html +15 -0
- oldman/web/templates/oldman/tables/default/summary.html +5 -0
- oldman/web/websocket/__init__.py +46 -0
- oldman-0.1.0.dist-info/METADATA +157 -0
- oldman-0.1.0.dist-info/RECORD +517 -0
- oldman-0.1.0.dist-info/WHEEL +4 -0
- oldman-0.1.0.dist-info/entry_points.txt +2 -0
- oldman-0.1.0.dist-info/licenses/LICENSE +21 -0
oldman/__init__.py
ADDED
oldman/apps/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@author:alex
|
|
3
|
+
@date:2026/7/14
|
|
4
|
+
@time:18:44
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__author__ = "alex"
|
|
8
|
+
|
|
9
|
+
from oldman.apps.config import (
|
|
10
|
+
AppConfig,
|
|
11
|
+
AppNotInstalledError,
|
|
12
|
+
AppSettingsNotDefinedError,
|
|
13
|
+
AppSettingsNotReadyError,
|
|
14
|
+
)
|
|
15
|
+
from oldman.apps.registry import AppRegistry
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"AppConfig",
|
|
19
|
+
"AppNotInstalledError",
|
|
20
|
+
"AppRegistry",
|
|
21
|
+
"AppSettingsNotDefinedError",
|
|
22
|
+
"AppSettingsNotReadyError",
|
|
23
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Built-in Admin application."""
|
|
2
|
+
|
|
3
|
+
from oldman.apps.admin.model_admin import AdminUserModelAdmin, ModelAdmin
|
|
4
|
+
from oldman.apps.admin.runtime import install_admin
|
|
5
|
+
from oldman.apps.admin.site import AdminSite, site
|
|
6
|
+
from oldman.apps.admin.users import AdminUserManagementError
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AdminSite",
|
|
10
|
+
"AdminUserManagementError",
|
|
11
|
+
"AdminUserModelAdmin",
|
|
12
|
+
"ModelAdmin",
|
|
13
|
+
"install_admin",
|
|
14
|
+
"site",
|
|
15
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Installable Admin application metadata."""
|
|
2
|
+
|
|
3
|
+
from oldman.apps import AppConfig
|
|
4
|
+
from oldman.apps.admin.settings import AdminSettings
|
|
5
|
+
from oldman.i18n import gettext_lazy as _
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AdminAppConfig(AppConfig[AdminSettings]):
|
|
9
|
+
"""Describe the built-in Admin application and its settings owner."""
|
|
10
|
+
|
|
11
|
+
label = "admin"
|
|
12
|
+
display_name = _("Administration")
|
|
13
|
+
icon = "ri-admin-line"
|
|
14
|
+
settings_model = AdminSettings
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
app = AdminAppConfig()
|
|
18
|
+
|
|
19
|
+
__all__ = ["AdminAppConfig", "app"]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""User administration commands provided by the built-in Admin app."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from oldman.auth import (
|
|
8
|
+
change_user_password,
|
|
9
|
+
ensure_superuser,
|
|
10
|
+
get_user_by_username,
|
|
11
|
+
user_identity,
|
|
12
|
+
)
|
|
13
|
+
from oldman.cli import Command
|
|
14
|
+
from oldman.i18n import gettext
|
|
15
|
+
from oldman.i18n import gettext_lazy as _
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _username(value: str | None) -> str:
|
|
19
|
+
"""Return one non-empty username from an option or interactive prompt."""
|
|
20
|
+
username = (value if value is not None else typer.prompt(gettext("Username"))).strip()
|
|
21
|
+
if not username:
|
|
22
|
+
raise ValueError(gettext("Username cannot be empty."))
|
|
23
|
+
return username
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _prompt_password() -> str:
|
|
27
|
+
"""Read one password without exposing it in command arguments or terminal output."""
|
|
28
|
+
return str(
|
|
29
|
+
typer.prompt(
|
|
30
|
+
gettext("Password"),
|
|
31
|
+
hide_input=True,
|
|
32
|
+
confirmation_prompt=gettext("Confirm Password"),
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CreateSuperuser(Command):
|
|
38
|
+
"""Create one new active staff superuser."""
|
|
39
|
+
|
|
40
|
+
name = "createsuperuser"
|
|
41
|
+
help = _("Create Superuser")
|
|
42
|
+
|
|
43
|
+
async def handle(
|
|
44
|
+
self,
|
|
45
|
+
username: str | None = None,
|
|
46
|
+
email: str | None = None,
|
|
47
|
+
) -> str:
|
|
48
|
+
"""Prompt for missing credentials and create one configured User."""
|
|
49
|
+
selected_username = _username(username)
|
|
50
|
+
if await get_user_by_username(selected_username) is not None:
|
|
51
|
+
raise ValueError(gettext("Username already exists"))
|
|
52
|
+
|
|
53
|
+
selected_email = (
|
|
54
|
+
email
|
|
55
|
+
if email is not None
|
|
56
|
+
else str(
|
|
57
|
+
typer.prompt(
|
|
58
|
+
gettext("Email"),
|
|
59
|
+
default="",
|
|
60
|
+
show_default=False,
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
await ensure_superuser(
|
|
65
|
+
selected_username,
|
|
66
|
+
_prompt_password(),
|
|
67
|
+
selected_email.strip() or None,
|
|
68
|
+
)
|
|
69
|
+
return gettext("User created")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ChangePassword(Command):
|
|
73
|
+
"""Change one existing configured User password."""
|
|
74
|
+
|
|
75
|
+
name = "changepassword"
|
|
76
|
+
help = _("Change Password")
|
|
77
|
+
|
|
78
|
+
async def handle(self, username: str) -> str:
|
|
79
|
+
"""Find the selected User and replace its password."""
|
|
80
|
+
selected_username = _username(username)
|
|
81
|
+
user = await get_user_by_username(selected_username)
|
|
82
|
+
if user is None:
|
|
83
|
+
raise ValueError(gettext("User not found."))
|
|
84
|
+
|
|
85
|
+
changed = await change_user_password(
|
|
86
|
+
user_identity(user),
|
|
87
|
+
_prompt_password(),
|
|
88
|
+
)
|
|
89
|
+
if changed is None:
|
|
90
|
+
raise ValueError(gettext("User not found."))
|
|
91
|
+
return gettext("Password changed")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
__all__ = ["ChangePassword", "CreateSuperuser"]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Admin model identity coercion helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid as uuid_pkg
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from sqlalchemy import Integer
|
|
10
|
+
from sqlalchemy.sql.schema import Column
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def coerce_value(column: Column[Any], value: Any) -> Any:
|
|
14
|
+
"""Coerce a form or identity value through one mapped column's Python type."""
|
|
15
|
+
value = first_value(value)
|
|
16
|
+
if value is None:
|
|
17
|
+
return None
|
|
18
|
+
if value == "" and column.nullable:
|
|
19
|
+
return None
|
|
20
|
+
try:
|
|
21
|
+
python_type = column.type.python_type
|
|
22
|
+
except (AttributeError, NotImplementedError):
|
|
23
|
+
return value
|
|
24
|
+
if isinstance(value, python_type):
|
|
25
|
+
return value
|
|
26
|
+
if python_type is bool:
|
|
27
|
+
normalized = str(value).strip().lower()
|
|
28
|
+
if normalized in {"1", "true", "yes", "on"}:
|
|
29
|
+
return True
|
|
30
|
+
if normalized in {"0", "false", "no", "off"}:
|
|
31
|
+
return False
|
|
32
|
+
raise ValueError(f"Invalid boolean value: {value!r}")
|
|
33
|
+
try:
|
|
34
|
+
if python_type is Decimal:
|
|
35
|
+
return Decimal(str(value))
|
|
36
|
+
if python_type is uuid_pkg.UUID:
|
|
37
|
+
return uuid_pkg.UUID(str(value))
|
|
38
|
+
return python_type(value)
|
|
39
|
+
except (ArithmeticError, TypeError, ValueError) as exc:
|
|
40
|
+
raise ValueError(f"Invalid {python_type.__name__} value: {value!r}") from exc
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def first_value(value: Any) -> Any:
|
|
44
|
+
"""Return a scalar value from Sanic form mappings."""
|
|
45
|
+
if isinstance(value, (list, tuple)):
|
|
46
|
+
return value[0] if value else None
|
|
47
|
+
return value
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def explicit_primary_key_column(mapper: Any, *, dialect: Any = None) -> Any | None:
|
|
51
|
+
"""Return a key that the active SQLAlchemy dialect cannot generate."""
|
|
52
|
+
primary_key = list(mapper.primary_key)
|
|
53
|
+
if len(primary_key) != 1:
|
|
54
|
+
raise ValueError("Admin models require exactly one primary key column")
|
|
55
|
+
column = primary_key[0]
|
|
56
|
+
if column.default is not None or column.server_default is not None or column.identity is not None:
|
|
57
|
+
return None
|
|
58
|
+
if not isinstance(column.type, Integer) or column.autoincrement not in {True, "auto", "ignore_fk"}:
|
|
59
|
+
return column
|
|
60
|
+
if getattr(column.table, "_autoincrement_column", None) is not column:
|
|
61
|
+
return column
|
|
62
|
+
|
|
63
|
+
if dialect is None:
|
|
64
|
+
# With no bound session, keep the framework's portable Integer default
|
|
65
|
+
# while requiring dialect-sensitive subclasses such as BigInteger.
|
|
66
|
+
return None if type(column.type) is Integer else column
|
|
67
|
+
|
|
68
|
+
dialect_name = str(getattr(dialect, "name", "")).lower()
|
|
69
|
+
if dialect_name == "sqlite":
|
|
70
|
+
# SQLite generates rowids only when the dialect renders the declared
|
|
71
|
+
# type as the exact token INTEGER (including Integer with_variant()).
|
|
72
|
+
rendered_type = str(column.type.compile(dialect=dialect)).strip().upper()
|
|
73
|
+
if rendered_type == "INTEGER":
|
|
74
|
+
return None
|
|
75
|
+
return column
|
|
76
|
+
if dialect_name in {"cockroachdb", "mariadb", "mssql", "mysql", "postgresql"}:
|
|
77
|
+
return None
|
|
78
|
+
return column
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def session_dialect(session: Any) -> Any | None:
|
|
82
|
+
"""Return the SQLAlchemy dialect bound to a sync or async session."""
|
|
83
|
+
if session is None:
|
|
84
|
+
return None
|
|
85
|
+
bind = None
|
|
86
|
+
get_bind = getattr(session, "get_bind", None)
|
|
87
|
+
if callable(get_bind):
|
|
88
|
+
try:
|
|
89
|
+
bind = get_bind()
|
|
90
|
+
except (AttributeError, NotImplementedError):
|
|
91
|
+
bind = None
|
|
92
|
+
if bind is None:
|
|
93
|
+
bind = getattr(session, "bind", None)
|
|
94
|
+
return getattr(bind, "dialect", None)
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Forms for the built-in Admin user manager."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import cache
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import inspect as sqlalchemy_inspect
|
|
9
|
+
from sqlalchemy import select
|
|
10
|
+
from wtforms import (
|
|
11
|
+
BooleanField,
|
|
12
|
+
DateTimeLocalField,
|
|
13
|
+
PasswordField,
|
|
14
|
+
SelectField,
|
|
15
|
+
StringField,
|
|
16
|
+
)
|
|
17
|
+
from wtforms.validators import DataRequired, InputRequired, Length, Optional, Regexp
|
|
18
|
+
|
|
19
|
+
from oldman.apps.admin.crud import explicit_primary_key_column, session_dialect
|
|
20
|
+
from oldman.apps.admin.users import (
|
|
21
|
+
admin_user_identity_matches,
|
|
22
|
+
change_admin_user_password,
|
|
23
|
+
)
|
|
24
|
+
from oldman.i18n import gettext, gettext_lazy
|
|
25
|
+
from oldman.web.auth.forms import PASSWORD_MESSAGE, PASSWORD_PATTERN
|
|
26
|
+
from oldman.web.components.forms import (
|
|
27
|
+
DateTimePickerWidget,
|
|
28
|
+
FieldLayout,
|
|
29
|
+
TailwindForm,
|
|
30
|
+
TailwindModelForm,
|
|
31
|
+
TailwindTableFilterForm,
|
|
32
|
+
)
|
|
33
|
+
from oldman.web.components.forms.models import model_field_for_column
|
|
34
|
+
|
|
35
|
+
BOOLEAN_FILTER_CHOICES = (
|
|
36
|
+
("", cast(str, gettext_lazy("All"))),
|
|
37
|
+
("true", cast(str, gettext_lazy("Yes"))),
|
|
38
|
+
("false", cast(str, gettext_lazy("No"))),
|
|
39
|
+
)
|
|
40
|
+
EDITABLE_USER_FIELDS = ("username", "email", "display_name", "is_active", "is_staff", "is_superuser")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AdminLoginForm(TailwindForm):
|
|
44
|
+
"""Collect the built-in Admin login credentials."""
|
|
45
|
+
|
|
46
|
+
username = StringField(
|
|
47
|
+
cast(str, gettext_lazy("Username")),
|
|
48
|
+
id="username",
|
|
49
|
+
validators=[DataRequired()],
|
|
50
|
+
render_kw={
|
|
51
|
+
"required": True,
|
|
52
|
+
"autocomplete": "username",
|
|
53
|
+
"placeholder": gettext_lazy("Enter username"),
|
|
54
|
+
"autofocus": True,
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
password = PasswordField(
|
|
58
|
+
cast(str, gettext_lazy("Password")),
|
|
59
|
+
id="password-input",
|
|
60
|
+
validators=[DataRequired()],
|
|
61
|
+
render_kw={
|
|
62
|
+
"required": True,
|
|
63
|
+
"autocomplete": "current-password",
|
|
64
|
+
"placeholder": gettext_lazy("Enter password"),
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class AdminUserFilterForm(TailwindTableFilterForm):
|
|
70
|
+
"""Filter the configured Admin user table."""
|
|
71
|
+
|
|
72
|
+
q = StringField(
|
|
73
|
+
cast(str, gettext_lazy("Search")),
|
|
74
|
+
render_kw={"type": "search", "placeholder": gettext_lazy("Search username, email, display name")},
|
|
75
|
+
)
|
|
76
|
+
is_active = SelectField(cast(str, gettext_lazy("Active")), choices=BOOLEAN_FILTER_CHOICES, validators=[Optional()])
|
|
77
|
+
is_staff = SelectField(cast(str, gettext_lazy("Staff")), choices=BOOLEAN_FILTER_CHOICES, validators=[Optional()])
|
|
78
|
+
is_superuser = SelectField(cast(str, gettext_lazy("Superuser")), choices=BOOLEAN_FILTER_CHOICES, validators=[Optional()])
|
|
79
|
+
last_login_from = DateTimeLocalField(
|
|
80
|
+
cast(str, gettext_lazy("Last Login From")),
|
|
81
|
+
format="%Y-%m-%dT%H:%M",
|
|
82
|
+
validators=[Optional()],
|
|
83
|
+
widget=DateTimePickerWidget(date_format="Y-m-d\\TH:i"),
|
|
84
|
+
)
|
|
85
|
+
last_login_to = DateTimeLocalField(
|
|
86
|
+
cast(str, gettext_lazy("Last Login To")),
|
|
87
|
+
format="%Y-%m-%dT%H:%M",
|
|
88
|
+
validators=[Optional()],
|
|
89
|
+
widget=DateTimePickerWidget(date_format="Y-m-d\\TH:i"),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
field_layout = (
|
|
93
|
+
FieldLayout("q", "lg:col-span-4 md:col-span-6"),
|
|
94
|
+
FieldLayout("is_active", "lg:col-span-2 md:col-span-6"),
|
|
95
|
+
FieldLayout("is_staff", "lg:col-span-2 md:col-span-6"),
|
|
96
|
+
FieldLayout("is_superuser", "lg:col-span-2 md:col-span-6"),
|
|
97
|
+
FieldLayout("last_login_from", "lg:col-span-2 md:col-span-6"),
|
|
98
|
+
FieldLayout("last_login_to", "lg:col-span-2 md:col-span-6"),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class AdminUserModelForm(TailwindModelForm):
|
|
103
|
+
"""Shared validation and persistence for configured Admin user models."""
|
|
104
|
+
|
|
105
|
+
current_user_id: int | None = None
|
|
106
|
+
confirm_password: Any
|
|
107
|
+
email: Any
|
|
108
|
+
password: Any
|
|
109
|
+
username: Any
|
|
110
|
+
|
|
111
|
+
async def clean_username(self) -> str:
|
|
112
|
+
"""Validate username uniqueness before a database constraint can fail."""
|
|
113
|
+
username = str(self.username.data or "").strip()
|
|
114
|
+
if self.session is None or not username:
|
|
115
|
+
return username
|
|
116
|
+
result = await self.session.execute(select(self.model).where(self.model.username == username))
|
|
117
|
+
existing = result.scalar_one_or_none()
|
|
118
|
+
if existing is not None and not self.is_same_instance(existing):
|
|
119
|
+
self.add_error("username", gettext("Username already exists"))
|
|
120
|
+
return username
|
|
121
|
+
|
|
122
|
+
async def clean_email(self) -> str | None:
|
|
123
|
+
"""Validate optional email uniqueness before persistence."""
|
|
124
|
+
email = str(self.email.data or "").strip()
|
|
125
|
+
if not email:
|
|
126
|
+
return None
|
|
127
|
+
if self.session is None:
|
|
128
|
+
return email
|
|
129
|
+
result = await self.session.execute(select(self.model).where(self.model.email == email))
|
|
130
|
+
existing = result.scalar_one_or_none()
|
|
131
|
+
if existing is not None and not self.is_same_instance(existing):
|
|
132
|
+
self.add_error("email", gettext("Email already exists"))
|
|
133
|
+
return email
|
|
134
|
+
|
|
135
|
+
async def clean_explicit_primary_key(self) -> Any:
|
|
136
|
+
"""Validate a user-supplied primary key before database persistence."""
|
|
137
|
+
primary_key = list(self.mapper.primary_key)[0]
|
|
138
|
+
field_name = primary_key.key
|
|
139
|
+
value = self._fields[field_name].data
|
|
140
|
+
if self.session is None or value is None or value == "":
|
|
141
|
+
return value
|
|
142
|
+
result = await self.session.execute(select(self.model).where(getattr(self.model, field_name) == value))
|
|
143
|
+
existing = result.scalar_one_or_none()
|
|
144
|
+
if existing is not None and not self.is_same_instance(existing):
|
|
145
|
+
self.add_error(
|
|
146
|
+
field_name,
|
|
147
|
+
gettext("%(field)s already exists", field=self._fields[field_name].label.text),
|
|
148
|
+
)
|
|
149
|
+
return value
|
|
150
|
+
|
|
151
|
+
async def clean(self) -> None:
|
|
152
|
+
"""Protect the current user and validate password confirmation."""
|
|
153
|
+
await super().clean()
|
|
154
|
+
if self.instance is not None and admin_user_identity_matches(self.instance, self.current_user_id):
|
|
155
|
+
if self.cleaned_data.get("is_active") is False:
|
|
156
|
+
self.add_error("is_active", gettext("cannot disable current user"))
|
|
157
|
+
if self.cleaned_data.get("is_staff") is False:
|
|
158
|
+
self.add_error("is_staff", gettext("cannot remove current user staff access"))
|
|
159
|
+
if self.cleaned_data.get("is_superuser") is False:
|
|
160
|
+
self.add_error("is_superuser", gettext("cannot remove current user superuser access"))
|
|
161
|
+
if "confirm_password" in self._fields and self.password.data != self.confirm_password.data:
|
|
162
|
+
self.add_error("confirm_password", gettext("Passwords do not match"))
|
|
163
|
+
|
|
164
|
+
async def save(self, *, commit: bool = False, session: Any = None) -> Any:
|
|
165
|
+
"""Persist profile fields and hash the create-form password."""
|
|
166
|
+
user = await super().save(commit=False)
|
|
167
|
+
if bool(getattr(user, "is_superuser", False)):
|
|
168
|
+
user.is_staff = True
|
|
169
|
+
if "password" in self._fields:
|
|
170
|
+
change_admin_user_password(user, str(self.password.data or ""))
|
|
171
|
+
if commit:
|
|
172
|
+
active_session = session or self.session
|
|
173
|
+
if active_session is None:
|
|
174
|
+
raise ValueError("save(commit=True) requires a session")
|
|
175
|
+
active_session.add(user)
|
|
176
|
+
flush_result = active_session.flush()
|
|
177
|
+
import inspect
|
|
178
|
+
|
|
179
|
+
if inspect.isawaitable(flush_result):
|
|
180
|
+
await flush_result
|
|
181
|
+
return user
|
|
182
|
+
|
|
183
|
+
def is_same_instance(self, other: Any) -> bool:
|
|
184
|
+
"""Compare rows without assuming the primary key is named ``id``."""
|
|
185
|
+
if self.instance is None:
|
|
186
|
+
return False
|
|
187
|
+
try:
|
|
188
|
+
primary_key = list(self.mapper.primary_key)
|
|
189
|
+
return len(primary_key) == 1 and getattr(other, primary_key[0].key) == getattr(self.instance, primary_key[0].key)
|
|
190
|
+
except (AttributeError, TypeError):
|
|
191
|
+
return other is self.instance
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def mapper(self):
|
|
195
|
+
"""Return the mapped model metadata used by identity comparisons."""
|
|
196
|
+
from sqlalchemy import inspect as sqlalchemy_inspect
|
|
197
|
+
|
|
198
|
+
return sqlalchemy_inspect(self.model)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@cache
|
|
202
|
+
def admin_user_edit_form_class(user_model: type[Any]) -> type[AdminUserModelForm]:
|
|
203
|
+
"""Build a shared ModelForm bound to the configured user model."""
|
|
204
|
+
return _admin_user_model_form_class(user_model, create=False, include_explicit_primary_key=False)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def admin_user_create_form_class(user_model: type[Any], *, session: Any = None, dialect: Any = None) -> type[AdminUserModelForm]:
|
|
208
|
+
"""Build a password-aware ModelForm bound to the configured user model."""
|
|
209
|
+
mapper = sqlalchemy_inspect(user_model)
|
|
210
|
+
resolved_dialect = dialect or session_dialect(session)
|
|
211
|
+
include_primary_key = explicit_primary_key_column(mapper, dialect=resolved_dialect) is not None
|
|
212
|
+
return _admin_user_model_form_class(
|
|
213
|
+
user_model,
|
|
214
|
+
create=True,
|
|
215
|
+
include_explicit_primary_key=include_primary_key,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@cache
|
|
220
|
+
def _admin_user_model_form_class(
|
|
221
|
+
user_model: type[Any],
|
|
222
|
+
*,
|
|
223
|
+
create: bool,
|
|
224
|
+
include_explicit_primary_key: bool,
|
|
225
|
+
) -> type[AdminUserModelForm]:
|
|
226
|
+
mapper = sqlalchemy_inspect(user_model)
|
|
227
|
+
model_fields = list(EDITABLE_USER_FIELDS)
|
|
228
|
+
field_layout: list[FieldLayout] = [
|
|
229
|
+
FieldLayout("username", "md:col-span-6"),
|
|
230
|
+
FieldLayout("email", "md:col-span-6"),
|
|
231
|
+
FieldLayout("display_name", "md:col-span-6"),
|
|
232
|
+
]
|
|
233
|
+
fields: dict[str, Any] = {
|
|
234
|
+
"username": StringField(
|
|
235
|
+
cast(str, gettext_lazy("Username")),
|
|
236
|
+
validators=[DataRequired(), Length(max=150)],
|
|
237
|
+
render_kw={"required": True, "maxlength": 150},
|
|
238
|
+
),
|
|
239
|
+
"email": StringField(
|
|
240
|
+
cast(str, gettext_lazy("Email")),
|
|
241
|
+
validators=[Optional(), Length(max=254)],
|
|
242
|
+
render_kw={"maxlength": 254},
|
|
243
|
+
),
|
|
244
|
+
"display_name": StringField(
|
|
245
|
+
cast(str, gettext_lazy("Display Name")),
|
|
246
|
+
validators=[Optional(), Length(max=150)],
|
|
247
|
+
render_kw={"maxlength": 150},
|
|
248
|
+
),
|
|
249
|
+
"is_active": BooleanField(cast(str, gettext_lazy("Active"))),
|
|
250
|
+
"is_staff": BooleanField(cast(str, gettext_lazy("Staff"))),
|
|
251
|
+
"is_superuser": BooleanField(cast(str, gettext_lazy("Superuser"))),
|
|
252
|
+
}
|
|
253
|
+
primary_key = list(mapper.primary_key)[0]
|
|
254
|
+
if not create and primary_key.key in model_fields:
|
|
255
|
+
# Session identity is backed by the persisted primary key. Editing it
|
|
256
|
+
# would orphan the active session and bypass the source self-protection
|
|
257
|
+
# rules, so custom protocol models may only collect it during create.
|
|
258
|
+
model_fields.remove(primary_key.key)
|
|
259
|
+
fields.pop(primary_key.key, None)
|
|
260
|
+
field_layout = [item for item in field_layout if item.name != primary_key.key]
|
|
261
|
+
if create:
|
|
262
|
+
if include_explicit_primary_key:
|
|
263
|
+
field_name = primary_key.key
|
|
264
|
+
if field_name not in fields:
|
|
265
|
+
fields[field_name] = model_field_for_column(primary_key, mapper=mapper)
|
|
266
|
+
model_fields.insert(0, field_name)
|
|
267
|
+
field_layout.insert(0, FieldLayout(field_name, "md:col-span-6"))
|
|
268
|
+
validators = list(fields[field_name].kwargs.get("validators", []))
|
|
269
|
+
if fields[field_name].field_class is not BooleanField and not any(
|
|
270
|
+
isinstance(validator, (DataRequired, InputRequired)) for validator in validators
|
|
271
|
+
):
|
|
272
|
+
fields[field_name].kwargs["validators"] = [InputRequired(), *validators]
|
|
273
|
+
if not hasattr(AdminUserModelForm, f"clean_{field_name}"):
|
|
274
|
+
fields[f"clean_{field_name}"] = AdminUserModelForm.clean_explicit_primary_key
|
|
275
|
+
fields.update(
|
|
276
|
+
password=PasswordField(
|
|
277
|
+
cast(str, gettext_lazy("Password")),
|
|
278
|
+
validators=[
|
|
279
|
+
DataRequired(),
|
|
280
|
+
Length(min=8, max=128),
|
|
281
|
+
Regexp(PASSWORD_PATTERN, message=PASSWORD_MESSAGE),
|
|
282
|
+
],
|
|
283
|
+
render_kw={
|
|
284
|
+
"required": True,
|
|
285
|
+
"minlength": 8,
|
|
286
|
+
"maxlength": 128,
|
|
287
|
+
"pattern": PASSWORD_PATTERN,
|
|
288
|
+
"autocomplete": "new-password",
|
|
289
|
+
},
|
|
290
|
+
),
|
|
291
|
+
confirm_password=PasswordField(
|
|
292
|
+
cast(str, gettext_lazy("Confirm Password")),
|
|
293
|
+
validators=[DataRequired(), Length(min=8, max=128)],
|
|
294
|
+
render_kw={"required": True, "minlength": 8, "maxlength": 128, "autocomplete": "new-password"},
|
|
295
|
+
),
|
|
296
|
+
)
|
|
297
|
+
field_layout.extend(
|
|
298
|
+
(
|
|
299
|
+
FieldLayout("password", "md:col-span-6"),
|
|
300
|
+
FieldLayout("confirm_password", "md:col-span-6"),
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
field_layout.extend(
|
|
304
|
+
(
|
|
305
|
+
FieldLayout("is_active", "md:col-span-4"),
|
|
306
|
+
FieldLayout("is_staff", "md:col-span-4"),
|
|
307
|
+
FieldLayout("is_superuser", "md:col-span-4"),
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
fields["field_layout"] = tuple(field_layout)
|
|
311
|
+
fields["Meta"] = type("Meta", (), {"model": user_model, "fields": model_fields})
|
|
312
|
+
suffix = "CreateForm" if create else "EditForm"
|
|
313
|
+
return type(f"{user_model.__name__}{suffix}", (AdminUserModelForm,), fields)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
__all__ = [
|
|
317
|
+
"AdminLoginForm",
|
|
318
|
+
"AdminUserFilterForm",
|
|
319
|
+
"PASSWORD_MESSAGE",
|
|
320
|
+
"PASSWORD_PATTERN",
|
|
321
|
+
"admin_user_create_form_class",
|
|
322
|
+
"admin_user_edit_form_class",
|
|
323
|
+
]
|
|
Binary file
|
|
Binary file
|