From 49769abfa9e7235ceebce4fb812b96bc514d6c46 Mon Sep 17 00:00:00 2001 From: sax Date: Thu, 23 Nov 2023 20:32:15 +0100 Subject: [PATCH] updates - fixes CSP --- .flake8 | 8 +- .pre-commit-config.yaml | 58 +- exorcist.py | 64 - pyproject.toml | 35 +- src/aurora/api/router.py | 2 +- src/aurora/api/urls.py | 2 +- src/aurora/api/viewsets/registration.py | 7 +- src/aurora/config/asgi.py | 17 +- src/aurora/config/settings.py | 39 +- src/aurora/core/admin/field_editor.py | 4 +- src/aurora/core/admin/mixin.py | 1311 +++++++++-------- src/aurora/core/admin/project.py | 5 +- src/aurora/core/backends.py | 22 +- src/aurora/core/channels.py | 90 +- src/aurora/core/fields/select.py | 2 +- src/aurora/core/fields/widgets/datetime.py | 2 +- .../migrations/0054_auto_20231123_0605.py | 26 + .../migrations/0055_alter_project_slug.py | 18 + src/aurora/core/models.py | 27 +- src/aurora/core/templates/smart/_form.html | 2 +- src/aurora/core/version_media.py | 3 +- .../templates/counters/chart_base.html | 5 + .../templates/counters/chart_month.html | 2 +- .../counters/templates/counters/index.html | 6 +- .../counters/templates/counters/project.html | 16 + src/aurora/counters/urls.py | 9 +- src/aurora/counters/views.py | 36 +- src/aurora/i18n/translate.py | 3 +- src/aurora/management/commands/upgrade.py | 12 +- src/aurora/registration/admin/filters.py | 4 +- src/aurora/registration/admin/protocol.py | 39 +- src/aurora/registration/admin/registration.py | 30 +- src/aurora/registration/debug.py | 95 +- .../migrations/0051_auto_20231123_0605.py | 37 + src/aurora/registration/models.py | 68 +- src/aurora/registration/strategies.py | 2 +- .../templates/registration/register.html | 5 +- src/aurora/registration/views/registration.py | 14 +- src/aurora/security/backend.py | 19 +- src/aurora/security/models.py | 23 +- src/aurora/security/views.py | 10 +- src/aurora/web/middlewares/http2.py | 170 --- src/aurora/web/templates/base.html | 6 +- src/aurora/web/templatetags/http2.py | 18 - src/dbtemplates/admin.py | 2 +- .../static/dbtemplates/css/django.css | 1 + tests/.coveragerc | 2 + tests/conftest.py | 19 +- tests/extras/testutils/factories.py | 34 +- tests/extras/testutils/perms.py | 24 +- tests/test_admin.py | 8 +- tests/test_crypt.py | 8 +- tests/test_form.py | 16 +- tests/test_security.py | 27 +- tests/test_sync.py | 18 +- tests/test_utils.py | 4 +- tox.css | 17 - 57 files changed, 1287 insertions(+), 1266 deletions(-) delete mode 100644 exorcist.py create mode 100644 src/aurora/core/migrations/0054_auto_20231123_0605.py create mode 100644 src/aurora/core/migrations/0055_alter_project_slug.py create mode 100644 src/aurora/counters/templates/counters/project.html create mode 100644 src/aurora/registration/migrations/0051_auto_20231123_0605.py delete mode 100644 src/aurora/web/middlewares/http2.py delete mode 100644 src/aurora/web/templatetags/http2.py delete mode 100644 tox.css diff --git a/.flake8 b/.flake8 index 918e4d4d..628307c6 100644 --- a/.flake8 +++ b/.flake8 @@ -1,11 +1,11 @@ [flake8] max-complexity = 20 max-line-length = 120 -exclude = - ~* -ignore = E401,W391,E128,E261,E731,Q000,W504,W606,W503 -putty-ignore = +exclude = ~* +ignore = E401,W391,E128,E261,E731,Q000,W504,W606,W503,E203 +;putty-ignore = ; tests/test_choice_as_instance.py : E501 per-file-ignores = */__init__.py:F401,F403 + */migrations/*:E501 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3eadb537..ef8012a7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,51 @@ exclude: '^$' fail_fast: false repos: + - repo: local + hooks: + # Configuration for black exists in pyproject.toml, + # but we let pre-commit take care of the file filtering. + - id: black + name: black + args: [ '--check' ] + entry: black + language: python + types: [ python ] + require_serial: true + additional_dependencies: [black] + + # Configuration for isort exists in pyproject.toml, + # but we let pre-commit take care of the file filtering. + - id: isort + name: isort + args: [ '--version'] +# args: [ '--src', 'src/','--check-only' ] + entry: isort + language: python + types: [ python ] + additional_dependencies: [isort] + # Configuration for black exists in .flake8, + # but we let pre-commit take care of the file filtering. + - id: flake8 + name: flake8 + args: ["--config", ".flake8"] + entry: flake8 + language: python + types: [ python ] + additional_dependencies: [flake8] + + + - repo: https://github.com/Yelp/detect-secrets + rev: 0.9.1 + hooks: + - id: detect-secrets + args: [ '--baseline', '.secrets.baseline' ] + exclude: (tests/.*|.*/tenant\.sql|Pipfile\.lock|.*\.js|.gitlab-ci.yml|poetry.lock) + stages: [ push ] + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.1.0 hooks: - # - id: double-quote-string-fixer - # stages: [commit] - id: debug-statements stages: [commit] @@ -41,17 +81,3 @@ repos: - -p - /print\(111/ stages: [commit] - -# - repo: https://github.com/psf/black -# rev: 22.1.0 -# hooks: -# - id: black -# stages: [ commit ] - - - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 - hooks: - - id: flake8 - additional_dependencies: - - flake8-black>=0.1.1 - language_version: python3 diff --git a/exorcist.py b/exorcist.py deleted file mode 100644 index d7a87416..00000000 --- a/exorcist.py +++ /dev/null @@ -1,64 +0,0 @@ -from time import sleep, time - -import requests -import sys - - -class COLORS: - HEADER = "\033[95m" - OKBLUE = "\033[94m" - OKCYAN = "\033[96m" - SUCCESS = "\033[92m" - WARNING = "\033[93m" - FAIL = "\033[91m" - RESET = "\033[0m" - BOLD = "\033[1m" - UNDERLINE = "\033[4m" - MARK = "\xE2\x9C\x94" - MARK1 = "\u2713" - MARK2 = "\u2714\u274c" - MARK3 = "\N{check mark}" - MARK4 = "✓" - PY = "\U0001F40D" - CHECK = "\N{BALLOT BOX WITH CHECK}" - UNCHECK = "\N{BALLOT BOX}" - - -if __name__ == "__main__": - if "--random" in sys.argv: - rnd = time - else: - rnd = lambda: "" - - if len(sys.argv) == 1: - urls = ["https://register.unicef.org/"] - else: - urls = [u for u in sys.argv[1:] if u.startswith("http")] - - latest_ref = {} - latest_ver = {} - lastest_version = None - while True: - seed = rnd() - for url in urls: - ret = requests.get(f"{url}?{seed}", headers={"User-Agent": "Exorcist/1.0 "}) - ver = ret.headers.get("X-Aurora-Version", "N/A") - if lastest_version is not None: - if ver != lastest_version: - marker = COLORS.WARNING - if ver != latest_ver.get(url): - marker = COLORS.FAIL - else: - marker = COLORS.RESET - print( - f"{marker}...{url[-20:]} - {ret.status_code} - " - f"{ver:<7} - " - f"{ret.headers.get('X-Aurora-Build', 'N/A'):<16} - " - f"{ret.headers.get('X-Aurora-Time', 'N/A')} - " - f"{ret.headers.get('ETag', 'N/A')[:20]}{COLORS.RESET}" - ) - lastest_version = latest_ver[url] = ver - if len(urls) > 1: - print("=====") - - sleep(1) diff --git a/pyproject.toml b/pyproject.toml index 8b8f8334..6262e8e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,19 +99,38 @@ django-stubs = {extras = ["compatible-mypy"], version = "^1.16.0"} requires = ["poetry-core>=1.1.4"] build-backend = "poetry.core.masonry.api" -[tool.isort] -profile = "black" [tool.black] line-length = 120 -target-version = ['py39'] include = '\.pyi?$' exclude = ''' /( - \.toml - |\.sh - |\.git - |\.ini - |Dockerfile + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | venv + | _build + | buck-out + | build + | dist + | migrations + | snapshots )/ ''' + +[tool.isort] +combine_as_imports = true +default_section = "THIRDPARTY" +include_trailing_comma = true +known_django = "django" +known_typing = ["typing"] +known_tests = ["unittest", "pytest"] +known_app = "hope_country_report" +sections = ["FUTURE", 'TYPING', 'STDLIB',"TESTS",'DJANGO','THIRDPARTY','APP','LOCALFOLDER'] +multi_line_output = 3 +line_length = 120 +balanced_wrapping = true +order_by_type = false +skip_glob = ["**/migrations/**"] diff --git a/src/aurora/api/router.py b/src/aurora/api/router.py index 92c0b28b..62868f24 100644 --- a/src/aurora/api/router.py +++ b/src/aurora/api/router.py @@ -1,5 +1,5 @@ from django.http import HttpResponseForbidden -from rest_framework.routers import DefaultRouter, APIRootView +from rest_framework.routers import APIRootView, DefaultRouter class AuroraAPIRootView(APIRootView): diff --git a/src/aurora/api/urls.py b/src/aurora/api/urls.py index 42d4191d..a6d17bae 100644 --- a/src/aurora/api/urls.py +++ b/src/aurora/api/urls.py @@ -1,7 +1,7 @@ from django.urls import include, path -from .router import AuroraRouter from . import viewsets +from .router import AuroraRouter app_name = "api" diff --git a/src/aurora/api/viewsets/registration.py b/src/aurora/api/viewsets/registration.py index b6d3751b..da748b26 100644 --- a/src/aurora/api/viewsets/registration.py +++ b/src/aurora/api/viewsets/registration.py @@ -5,7 +5,7 @@ from collections import OrderedDict from urllib import parse -from django.http import HttpResponse, HttpRequest +from django.http import HttpRequest, HttpResponse from django.utils.cache import get_conditional_response from django_filters import rest_framework as filters from django_filters.rest_framework import DjangoFilterBackend @@ -17,7 +17,7 @@ from rest_framework.renderers import JSONRenderer from rest_framework.response import Response -from ...core.utils import get_etag, get_session_id, build_dict +from ...core.utils import build_dict, get_etag, get_session_id from ...registration.models import Record, Registration from ..serializers import RegistrationDetailSerializer, RegistrationListSerializer from ..serializers.record import DataTableRecordSerializer @@ -161,9 +161,8 @@ def csv(self, request: HttpRequest, pk): } """ reg: Registration = self.get_object() + from aurora.core.forms import CSVOptionsForm, DateFormatsForm from aurora.registration.forms import RegistrationExportForm - from aurora.core.forms import CSVOptionsForm - from aurora.core.forms import DateFormatsForm try: form = RegistrationExportForm(request.GET, initial=RegistrationExportForm.defaults) diff --git a/src/aurora/config/asgi.py b/src/aurora/config/asgi.py index acd6ab7e..6c4f979b 100644 --- a/src/aurora/config/asgi.py +++ b/src/aurora/config/asgi.py @@ -1,20 +1,21 @@ import os -from channels.auth import AuthMiddlewareStack -from channels.routing import ProtocolTypeRouter, URLRouter -from channels.security.websocket import AllowedHostsOriginValidator +# from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter + +# from channels.security.websocket import AllowedHostsOriginValidator from django.core.asgi import get_asgi_application -import aurora.core.channels +# import aurora.core.channels os.environ.setdefault("DJANGO_SETTINGS_MODULE", "aurora.config.settings") application = ProtocolTypeRouter( { "http": get_asgi_application(), - # Just HTTP for now. (We can add other protocols later.) - "websocket": AllowedHostsOriginValidator( - AuthMiddlewareStack(URLRouter(aurora.core.channels.websocket_urlpatterns)) - ), + # # Just HTTP for now. (We can add other protocols later.) + # "websocket": AllowedHostsOriginValidator( + # AuthMiddlewareStack(URLRouter(aurora.core.channels.websocket_urlpatterns)) + # ), } ) diff --git a/src/aurora/config/settings.py b/src/aurora/config/settings.py index b921fdb6..c2a27776 100644 --- a/src/aurora/config/settings.py +++ b/src/aurora/config/settings.py @@ -50,10 +50,9 @@ "reversion_compare", # https://github.com/jedie/django-reversion-compare "django_filters", # --- - # "aurora.admin.apps.AuroraAdminUIConfig", "smart_admin.apps.SmartLogsConfig", "smart_admin.apps.SmartTemplateConfig", - # "smart_admin.apps.SmartAuthConfig", + "smart_admin.apps.SmartAuthConfig", "smart_admin.apps.SmartConfig", "aurora.administration.apps.AuroraAdminConfig", "front_door.contrib", @@ -571,7 +570,7 @@ def show_ddt(request): # pragma: no-cover AZURE_TOKEN_URL = "https://login.microsoftonline.com/unicef.org/oauth2/token" # Social Auth settings. -SOCIAL_AUTH_BACKEND_NAME = 'macioce' +SOCIAL_AUTH_BACKEND_NAME = "macioce" SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET = env.str("AZURE_CLIENT_SECRET") SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_TENANT_ID = env("AZURE_TENANT_ID") SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY = env.str("AZURE_CLIENT_KEY") @@ -638,6 +637,7 @@ def show_ddt(request): # pragma: no-cover "browser.sentry-cdn.com", "cdnjs.cloudflare.com", "unisitetracker.unicef.io", + "cdn.jsdelivr.net", "register.unicef.org", "uni-hope-ukr-sr.azurefd.net", "uni-hope-ukr-sr-dev.azurefd.net", @@ -647,9 +647,7 @@ def show_ddt(request): # pragma: no-cover "csp.middleware.CSPMiddleware", ] CSP_DEFAULT_SRC = SOURCES -CSP_FRAME_ANCESTORS = ( - "'self'", -) +CSP_FRAME_ANCESTORS = ("'self'",) # CSP_SCRIPT_SRC = SOURCES # CSP_STYLE_SRC = ( # "'self'", @@ -671,20 +669,21 @@ def show_ddt(request): # pragma: no-cover # CSP_MEDIA_SRC = ("self",) # CSP_REPORT_URI = ("https://624948b721ea44ac2a6b4de4.endpoint.csper.io/?v=0;",) # CSP_WORKER_SRC = ("self",) -"""default-src 'self'; -script-src 'report-sample' 'self'; -style-src 'report-sample' 'self'; -object-src 'none'; -base-uri 'self'; -connect-src 'self'; -font-src 'self'; -frame-src 'self'; -img-src 'self'; -manifest-src 'self'; -media-src 'self'; -report-uri https://624948b721ea44ac2a6b4de4.endpoint.csper.io/?v=0; -worker-src 'none'; -""" +# """default-src 'self'; +# script-src 'report-sample' 'self'; +# style-src 'report-sample' 'self'; +# object-src 'none'; +# +# base-uri 'self'; +# connect-src 'self'; +# font-src 'self'; +# frame-src 'self'; +# img-src 'self'; +# manifest-src 'self'; +# media-src 'self'; +# report-uri https://624948b721ea44ac2a6b4de4.endpoint.csper.io/?v=0; +# worker-src 'none'; +# """ # CSP_INCLUDE_NONCE_IN = env("CSP_INCLUDE_NONCE_IN") # CSP_REPORT_ONLY = env("CSP_REPORT_ONLY") diff --git a/src/aurora/core/admin/field_editor.py b/src/aurora/core/admin/field_editor.py index 1d9f4fa0..fc349c00 100644 --- a/src/aurora/core/admin/field_editor.py +++ b/src/aurora/core/admin/field_editor.py @@ -1,8 +1,8 @@ import json -from django.conf import settings from typing import Dict from django import forms +from django.conf import settings from django.core.cache import caches from django.http import HttpResponse, HttpResponseRedirect, JsonResponse from django.shortcuts import render @@ -10,7 +10,7 @@ from django.utils.functional import cached_property from aurora.core.fields.widgets import JavascriptEditor -from aurora.core.forms import VersionMedia, FlexFormBaseForm +from aurora.core.forms import FlexFormBaseForm, VersionMedia from aurora.core.models import FlexFormField, OptionSet from aurora.core.utils import merge_data diff --git a/src/aurora/core/admin/mixin.py b/src/aurora/core/admin/mixin.py index 6e93f996..036bdf9c 100644 --- a/src/aurora/core/admin/mixin.py +++ b/src/aurora/core/admin/mixin.py @@ -1,682 +1,683 @@ -import logging - -from admin_extra_buttons.decorators import button -from admin_sync.utils import is_local -from concurrency.api import disable_concurrency -from django import forms -from django.conf import settings -from django.core.cache import caches -from reversion_compare.admin import CompareVersionAdmin -from ..utils import is_root - -logger = logging.getLogger(__name__) - -cache = caches["default"] - - -class ConcurrencyVersionAdmin(CompareVersionAdmin): - change_list_template = "admin_extra_buttons/change_list.html" - - @button(label="Recover deleted") - def _recoverlist_view(self, request): - return super().recoverlist_view(request) - - def reversion_register(self, model, **options): - options["exclude"] = ("version",) - super().reversion_register(model, **options) - - def revision_view(self, request, object_id, version_id, extra_context=None): - with disable_concurrency(): - return super().revision_view(request, object_id, version_id, extra_context) - - def recover_view(self, request, version_id, extra_context=None): - with disable_concurrency(): - return super().recover_view(request, version_id, extra_context) - - def has_change_permission(self, request, obj=None): - orig = super().has_change_permission(request, obj) - return orig and (settings.DEBUG or is_root(request) or is_local(request)) - - +# import logging # -# class Select2FieldComboFilter(ChoicesFieldComboFilter): -# template = "adminfilters/select2.html" +# from admin_extra_buttons.decorators import button +# from admin_sync.utils import is_local +# from concurrency.api import disable_concurrency +# from django import forms +# from django.conf import settings +# from django.core.cache import caches +# from reversion_compare.admin import CompareVersionAdmin # +# from ..utils import is_root # -# class Select2RelatedFieldComboFilter(RelatedFieldComboFilter): -# template = "adminfilters/select2.html" +# logger = logging.getLogger(__name__) # +# cache = caches["default"] # -# class ValidatorTestForm(forms.Form): -# code = forms.CharField( -# widget=JavascriptEditor, -# ) -# input = forms.CharField(widget=JavascriptEditor(toolbar=False), required=False) - # -# @register(Organization) -# class OrganizationAdmin(SyncMixin, MPTTModelAdmin): -# list_display = ("name",) -# mptt_level_indent = 20 -# mptt_indent_field = "name" -# search_fields = ("name",) -# protocol_class = AuroraSyncOrganizationProtocol -# change_list_template = "admin/core/organization/change_list.html" +# class ConcurrencyVersionAdmin(CompareVersionAdmin): +# change_list_template = "admin_extra_buttons/change_list.html" # -# def admin_sync_show_inspect(self): -# return True +# @button(label="Recover deleted") +# def _recoverlist_view(self, request): +# return super().recoverlist_view(request) # -# def get_readonly_fields(self, request, obj=None): -# ro = super().get_readonly_fields(request, obj) -# if obj and obj.pk: -# ro = list(ro) + ["slug"] -# return ro - +# def reversion_register(self, model, **options): +# options["exclude"] = ("version",) +# super().reversion_register(model, **options) # -# @register(Project) -# class ProjectAdmin(SyncMixin, MPTTModelAdmin): -# list_display = ("name",) -# list_filter = ("organization",) -# mptt_level_indent = 20 -# mptt_indent_field = "name" -# search_fields = ("name",) -# protocol_class = AuroraSyncProjectProtocol -# autocomplete_fields = "parent, " +# def revision_view(self, request, object_id, version_id, extra_context=None): +# with disable_concurrency(): +# return super().revision_view(request, object_id, version_id, extra_context) # -# def get_search_results(self, request, queryset, search_term): -# queryset, may_have_duplicates = super().get_search_results(request, queryset, search_term) -# if "oid" in request.GET: -# queryset = queryset.filter(organization__id=request.GET["oid"]) -# return queryset, may_have_duplicates +# def recover_view(self, request, version_id, extra_context=None): +# with disable_concurrency(): +# return super().recover_view(request, version_id, extra_context) # -# def get_readonly_fields(self, request, obj=None): -# ro = super().get_readonly_fields(request, obj) -# if obj and obj.pk: -# ro = list(ro) + ["slug"] -# return ro +# def has_change_permission(self, request, obj=None): +# orig = super().has_change_permission(request, obj) +# return orig and (settings.DEBUG or is_root(request) or is_local(request)) # # -# @register(Validator) -# class ValidatorAdmin(LoadDumpMixin, SyncMixin, ConcurrencyVersionAdmin, SmartModelAdmin): -# form = ValidatorForm -# list_editable = ("trace", "active", "draft") -# list_display = ("label", "name", "target", "used_by", "trace", "active", "draft") -# list_filter = ("target", "active", "draft", "trace") -# readonly_fields = ("version", "last_update_date") -# search_fields = ("name",) -# DEFAULTS = { -# Validator.FORM: {}, # cleaned data -# Validator.FIELD: "", # field value -# Validator.SCRIPT: "", # field value -# Validator.MODULE: [{}], -# Validator.FORMSET: {"total_form_count": 2, "errors": {}, "non_form_errors": {}, "cleaned_data": []}, -# } -# # change_list_template = "reversion/change_list.html" -# object_history_template = "reversion-compare/object_history.html" -# change_form_template = None -# inlines = [] -# -# def save_model(self, request, obj, form, change): -# super().save_model(request, obj, form, change) -# cache.set(f"validator-{request.user.pk}-{obj.pk}-status", obj.STATUS_UNKNOWN) -# -# def used_by(self, obj): -# if obj.target == Validator.FORM: -# return ", ".join(obj.flexform_set.values_list("name", flat=True)) -# elif obj.target == Validator.FIELD: -# return ", ".join(obj.flexformfield_set.values_list("name", flat=True)) -# elif obj.target == Validator.FORMSET: -# return ", ".join(obj.formset_set.values_list("name", flat=True)) -# elif obj.target == Validator.MODULE: -# return ", ".join(obj.validator_for.values_list("name", flat=True)) -# elif obj.target == Validator.SCRIPT: -# return ", ".join(obj.script_for.values_list("name", flat=True)) -# -# @button() -# def test(self, request, pk): -# ctx = self.get_common_context(request, pk) -# original = ctx["original"] -# stored = cache.get(f"validator-{request.user.pk}-{original.pk}-payload") -# ctx["traced"] = stored -# ctx["title"] = f"Test {original.target} validator: {original.name}" -# if stored: -# param = json.loads(stored) -# else: -# param = self.DEFAULTS[original.target] -# -# if request.method == "POST": -# form = ValidatorTestForm(request.POST) -# if form.is_valid(): -# self.object.code = form.cleaned_data["code"] -# self.object.save() -# # return HttpResponseRedirect("..") -# else: -# form = ValidatorTestForm( -# initial={"code": self.object.code, "input": original.jspickle(param)}, -# ) -# -# ctx["jslib"] = Validator.LIB -# ctx["is_script"] = self.object.target in [Validator.SCRIPT] -# ctx["is_validator"] = self.object.target not in [Validator.SCRIPT] -# ctx["form"] = form -# return render(request, "admin/core/validator/test.html", ctx) -# -# -# @register(FormSet) -# class FormSetAdmin(LoadDumpMixin, SyncMixin, SmartModelAdmin): -# list_display = ( -# "name", -# "title", -# "parent", -# "flex_form", -# "enabled", -# "validator", -# "min_num", -# "max_num", -# "extra", -# "dynamic", -# ) -# search_fields = ("name", "title") -# list_editable = ("enabled",) -# readonly_fields = ("version", "last_update_date") -# list_filter = ( -# ("parent", AutoCompleteFilter), -# ("flex_form", AutoCompleteFilter), -# ) -# formfield_overrides = { -# JSONField: {"widget": JSONEditor}, -# } -# -# def get_search_results(self, request, queryset, search_term): -# queryset, may_have_duplicates = super().get_search_results(request, queryset, search_term) -# if "oid" in request.GET: -# queryset = queryset.filter(flex_form__organization__id=request.GET["oid"]) -# return queryset, may_have_duplicates -# -# -# class FormSetInline(OrderableAdmin, TabularInline): -# model = FormSet -# fk_name = "parent" -# extra = 0 -# fields = ("name", "flex_form", "extra", "max_num", "min_num", "ordering") -# show_change_link = True -# ordering_field = "ordering" -# ordering_field_hide_input = True -# -# def formfield_for_dbfield(self, db_field, request, **kwargs): -# return super().formfield_for_dbfield(db_field, request, **kwargs) -# -# -# class FlexFormFieldFormInline(forms.ModelForm): -# class Meta: -# model = FlexFormField -# exclude = () -# -# def __init__(self, *args, **kwargs): -# super().__init__(*args, **kwargs) -# if self.instance.pk: -# self.fields["name"].widget.attrs = {"readonly": True, "tyle": "background-color:#f8f8f8;border:none"} -# -# -# class FlexFormFieldForm(forms.ModelForm): -# class Meta: -# model = FlexFormField -# exclude = () -# -# def clean(self): -# ret = super().clean() -# ret.setdefault("advanced", {}) -# dict_setdefault(ret["advanced"], FlexFormField.FLEX_FIELD_DEFAULT_ATTRS) -# dict_setdefault(ret["advanced"], {"kwargs": FIELD_KWARGS.get(ret["field_type"], {})}) -# return ret -# -# -# @register(FlexFormField) -# class FlexFormFieldAdmin(LoadDumpMixin, SyncMixin, ConcurrencyVersionAdmin, OrderableAdmin, SmartModelAdmin): -# search_fields = ("name", "label") -# list_display = ("label", "name", "flex_form", "field_type", "required", "enabled") -# list_editable = ["required", "enabled"] -# list_filter = ( -# ("flex_form", AutoCompleteFilter), -# ("field_type", Select2FieldComboFilter), -# QueryStringFilter, -# ) -# autocomplete_fields = ("flex_form", "validator") -# save_as = True -# formfield_overrides = { -# JSONField: {"widget": JSONEditor}, -# } -# form = FlexFormFieldForm -# ordering_field = "ordering" -# order = "ordering" -# readonly_fields = ("version", "last_update_date") -# -# def get_queryset(self, request): -# return super().get_queryset(request).select_related("flex_form") -# -# # change_list_template = "reversion/change_list.html" -# def get_readonly_fields(self, request, obj=None): -# if is_root(request): -# return [] -# else: -# return super().get_readonly_fields(request, obj) -# -# def field_type(self, obj): -# if obj.field_type: -# return obj.field_type.__name__ -# else: -# return "[[ removed ]]" -# -# def formfield_for_dbfield(self, db_field, request, **kwargs): -# if db_field.name == "advanced": -# kwargs["widget"] = JSONEditor() -# return super().formfield_for_dbfield(db_field, request, **kwargs) -# -# def formfield_for_choice_field(self, db_field, request, **kwargs): -# if db_field.name == "field_type": -# kwargs["widget"] = Select2Widget() -# return db_field.formfield(**kwargs) -# return super().formfield_for_choice_field(db_field, request, **kwargs) -# -# def get_changeform_initial_data(self, request): -# initial = super().get_changeform_initial_data(request) -# initial.setdefault("advanced", FlexFormField.FLEX_FIELD_DEFAULT_ATTRS) -# return initial -# -# @button(label="editor") -# def field_editor(self, request, pk): -# self.editor = FieldEditor(self, request, pk) -# if request.method == "POST": -# ret = self.editor.post(request, pk) -# self.message_user(request, "Saved", messages.SUCCESS) -# return ret -# else: -# return self.editor.get(request, pk) -# -# @view() -# def widget_attrs(self, request, pk): -# editor = FieldEditor(self, request, pk) -# return editor.get_configuration() -# -# @view() -# def widget_refresh(self, request, pk): -# editor = FieldEditor(self, request, pk) -# return editor.refresh() -# -# @view() -# def widget_code(self, request, pk): -# editor = FieldEditor(self, request, pk) -# return editor.get_code() -# -# @view() -# def widget_display(self, request, pk): -# editor = FieldEditor(self, request, pk) -# return editor.render() -# -# @button() -# def test(self, request, pk): -# ctx = self.get_common_context(request, pk) -# try: -# fld = ctx["original"] -# instance = fld.get_instance() -# ctx["debug_info"] = { -# # "widget": getattr(instance, "widget", None), -# "field_kwargs": fld.get_field_kwargs(), -# # "options": getattr(instance, "options", None), -# # "choices": getattr(instance, "choices", None), -# # "widget_attrs": instance.widget_attrs(instance.widget), -# } -# form_class_attrs = { -# "sample": instance, -# } -# form_class = type(forms.Form)("TestForm", (forms.Form,), form_class_attrs) -# -# if request.method == "POST": -# form = form_class(request.POST) -# -# if form.is_valid(): -# ctx["debug_info"]["cleaned_data"] = form.cleaned_data -# self.message_user( -# request, f"Form validation success. You have selected: {form.cleaned_data['sample']}" -# ) -# else: -# form = form_class() -# ctx["form"] = form -# ctx["instance"] = instance -# except Exception as e: -# logger.exception(e) -# ctx["error"] = e -# raise -# -# return render(request, "admin/core/flexformfield/test.html", ctx) - -# -# class FlexFormFieldInline(LoadDumpMixin, OrderableAdmin, TabularInline): -# template = "admin/core/flexformfield/tabular.html" -# model = FlexFormField -# form = FlexFormFieldFormInline -# fields = ("ordering", "label", "name", "required", "enabled", "field_type") -# show_change_link = True -# extra = 0 -# ordering_field = "ordering" -# ordering_field_hide_input = True -# -# def formfield_for_choice_field(self, db_field, request, **kwargs): -# # if db_field.name == "field_type": -# # kwargs["widget"] = Select2Widget() -# # return db_field.formfield(**kwargs) -# return super().formfield_for_choice_field(db_field, request, **kwargs) -# - - -class SyncConfigForm(forms.Form): - APPS = ("core", "registration") - apps = forms.MultipleChoiceField(choices=zip(APPS, APPS), widget=forms.CheckboxSelectMultiple()) - - -class SyncForm(SyncConfigForm): - host = forms.CharField() - username = forms.CharField() - password = forms.CharField(widget=forms.PasswordInput) - remember = forms.BooleanField(label="Remember me", required=False) - - -# -# class ProjectFilter(AutoCompleteFilter): -# fk_name = "project__organization__exact" -# -# def __init__(self, field, request, params, model, model_admin, field_path): -# self.request = request -# super().__init__(field, request, params, model, model_admin, field_path) -# -# def has_output(self): -# return "project__organization__exact" in self.request.GET +# # +# # class Select2FieldComboFilter(ChoicesFieldComboFilter): +# # template = "adminfilters/select2.html" +# # +# # +# # class Select2RelatedFieldComboFilter(RelatedFieldComboFilter): +# # template = "adminfilters/select2.html" +# # +# # +# # class ValidatorTestForm(forms.Form): +# # code = forms.CharField( +# # widget=JavascriptEditor, +# # ) +# # input = forms.CharField(widget=JavascriptEditor(toolbar=False), required=False) # -# def get_url(self): -# url = reverse("%s:autocomplete" % self.admin_site.name) -# if self.fk_name in self.request.GET: -# oid = self.request.GET[self.fk_name] -# return f"{url}?oid={oid}" -# return url +# # +# # @register(Organization) +# # class OrganizationAdmin(SyncMixin, MPTTModelAdmin): +# # list_display = ("name",) +# # mptt_level_indent = 20 +# # mptt_indent_field = "name" +# # search_fields = ("name",) +# # protocol_class = AuroraSyncOrganizationProtocol +# # change_list_template = "admin/core/organization/change_list.html" +# # +# # def admin_sync_show_inspect(self): +# # return True +# # +# # def get_readonly_fields(self, request, obj=None): +# # ro = super().get_readonly_fields(request, obj) +# # if obj and obj.pk: +# # ro = list(ro) + ["slug"] +# # return ro # # # -# # class UsedByRegistration(BaseAutoCompleteFilter): -# # def has_output(self): -# # return "project__exact" in self.request.GET +# # @register(Project) +# # class ProjectAdmin(SyncMixin, MPTTModelAdmin): +# # list_display = ("name",) +# # list_filter = ("organization",) +# # mptt_level_indent = 20 +# # mptt_indent_field = "name" +# # search_fields = ("name",) +# # protocol_class = AuroraSyncProjectProtocol +# # autocomplete_fields = "parent, " +# # +# # def get_search_results(self, request, queryset, search_term): +# # queryset, may_have_duplicates = super().get_search_results(request, queryset, search_term) +# # if "oid" in request.GET: +# # queryset = queryset.filter(organization__id=request.GET["oid"]) +# # return queryset, may_have_duplicates +# # +# # def get_readonly_fields(self, request, obj=None): +# # ro = super().get_readonly_fields(request, obj) +# # if obj and obj.pk: +# # ro = list(ro) + ["slug"] +# # return ro +# # +# # +# # @register(Validator) +# # class ValidatorAdmin(LoadDumpMixin, SyncMixin, ConcurrencyVersionAdmin, SmartModelAdmin): +# # form = ValidatorForm +# # list_editable = ("trace", "active", "draft") +# # list_display = ("label", "name", "target", "used_by", "trace", "active", "draft") +# # list_filter = ("target", "active", "draft", "trace") +# # readonly_fields = ("version", "last_update_date") +# # search_fields = ("name",) +# # DEFAULTS = { +# # Validator.FORM: {}, # cleaned data +# # Validator.FIELD: "", # field value +# # Validator.SCRIPT: "", # field value +# # Validator.MODULE: [{}], +# # Validator.FORMSET: {"total_form_count": 2, "errors": {}, "non_form_errors": {}, "cleaned_data": []}, +# # } +# # # change_list_template = "reversion/change_list.html" +# # object_history_template = "reversion-compare/object_history.html" +# # change_form_template = None +# # inlines = [] +# # +# # def save_model(self, request, obj, form, change): +# # super().save_model(request, obj, form, change) +# # cache.set(f"validator-{request.user.pk}-{obj.pk}-status", obj.STATUS_UNKNOWN) +# # +# # def used_by(self, obj): +# # if obj.target == Validator.FORM: +# # return ", ".join(obj.flexform_set.values_list("name", flat=True)) +# # elif obj.target == Validator.FIELD: +# # return ", ".join(obj.flexformfield_set.values_list("name", flat=True)) +# # elif obj.target == Validator.FORMSET: +# # return ", ".join(obj.formset_set.values_list("name", flat=True)) +# # elif obj.target == Validator.MODULE: +# # return ", ".join(obj.validator_for.values_list("name", flat=True)) +# # elif obj.target == Validator.SCRIPT: +# # return ", ".join(obj.script_for.values_list("name", flat=True)) +# # +# # @button() +# # def test(self, request, pk): +# # ctx = self.get_common_context(request, pk) +# # original = ctx["original"] +# # stored = cache.get(f"validator-{request.user.pk}-{original.pk}-payload") +# # ctx["traced"] = stored +# # ctx["title"] = f"Test {original.target} validator: {original.name}" +# # if stored: +# # param = json.loads(stored) +# # else: +# # param = self.DEFAULTS[original.target] +# # +# # if request.method == "POST": +# # form = ValidatorTestForm(request.POST) +# # if form.is_valid(): +# # self.object.code = form.cleaned_data["code"] +# # self.object.save() +# # # return HttpResponseRedirect("..") +# # else: +# # form = ValidatorTestForm( +# # initial={"code": self.object.code, "input": original.jspickle(param)}, +# # ) +# # +# # ctx["jslib"] = Validator.LIB +# # ctx["is_script"] = self.object.target in [Validator.SCRIPT] +# # ctx["is_validator"] = self.object.target not in [Validator.SCRIPT] +# # ctx["form"] = form +# # return render(request, "admin/core/validator/test.html", ctx) +# # +# # +# # @register(FormSet) +# # class FormSetAdmin(LoadDumpMixin, SyncMixin, SmartModelAdmin): +# # list_display = ( +# # "name", +# # "title", +# # "parent", +# # "flex_form", +# # "enabled", +# # "validator", +# # "min_num", +# # "max_num", +# # "extra", +# # "dynamic", +# # ) +# # search_fields = ("name", "title") +# # list_editable = ("enabled",) +# # readonly_fields = ("version", "last_update_date") +# # list_filter = ( +# # ("parent", AutoCompleteFilter), +# # ("flex_form", AutoCompleteFilter), +# # ) +# # formfield_overrides = { +# # JSONField: {"widget": JSONEditor}, +# # } +# # +# # def get_search_results(self, request, queryset, search_term): +# # queryset, may_have_duplicates = super().get_search_results(request, queryset, search_term) +# # if "oid" in request.GET: +# # queryset = queryset.filter(flex_form__organization__id=request.GET["oid"]) +# # return queryset, may_have_duplicates +# # +# # +# # class FormSetInline(OrderableAdmin, TabularInline): +# # model = FormSet +# # fk_name = "parent" +# # extra = 0 +# # fields = ("name", "flex_form", "extra", "max_num", "min_num", "ordering") +# # show_change_link = True +# # ordering_field = "ordering" +# # ordering_field_hide_input = True +# # +# # def formfield_for_dbfield(self, db_field, request, **kwargs): +# # return super().formfield_for_dbfield(db_field, request, **kwargs) +# # +# # +# # class FlexFormFieldFormInline(forms.ModelForm): +# # class Meta: +# # model = FlexFormField +# # exclude = () +# # +# # def __init__(self, *args, **kwargs): +# # super().__init__(*args, **kwargs) +# # if self.instance.pk: +# # self.fields["name"].widget.attrs = {"readonly": True, "tyle": "background-color:#f8f8f8;border:none"} +# # +# # +# # class FlexFormFieldForm(forms.ModelForm): +# # class Meta: +# # model = FlexFormField +# # exclude = () +# # +# # def clean(self): +# # ret = super().clean() +# # ret.setdefault("advanced", {}) +# # dict_setdefault(ret["advanced"], FlexFormField.FLEX_FIELD_DEFAULT_ATTRS) +# # dict_setdefault(ret["advanced"], {"kwargs": FIELD_KWARGS.get(ret["field_type"], {})}) +# # return ret +# # +# # +# # @register(FlexFormField) +# # class FlexFormFieldAdmin(LoadDumpMixin, SyncMixin, ConcurrencyVersionAdmin, OrderableAdmin, SmartModelAdmin): +# # search_fields = ("name", "label") +# # list_display = ("label", "name", "flex_form", "field_type", "required", "enabled") +# # list_editable = ["required", "enabled"] +# # list_filter = ( +# # ("flex_form", AutoCompleteFilter), +# # ("field_type", Select2FieldComboFilter), +# # QueryStringFilter, +# # ) +# # autocomplete_fields = ("flex_form", "validator") +# # save_as = True +# # formfield_overrides = { +# # JSONField: {"widget": JSONEditor}, +# # } +# # form = FlexFormFieldForm +# # ordering_field = "ordering" +# # order = "ordering" +# # readonly_fields = ("version", "last_update_date") # # -# # def queryset(self, request, queryset): -# # # {'registration__exact': '30'} -# # if not self.used_parameters: -# # return queryset +# # def get_queryset(self, request): +# # return super().get_queryset(request).select_related("flex_form") +# # +# # # change_list_template = "reversion/change_list.html" +# # def get_readonly_fields(self, request, obj=None): +# # if is_root(request): +# # return [] +# # else: +# # return super().get_readonly_fields(request, obj) +# # +# # def field_type(self, obj): +# # if obj.field_type: +# # return obj.field_type.__name__ +# # else: +# # return "[[ removed ]]" +# # +# # def formfield_for_dbfield(self, db_field, request, **kwargs): +# # if db_field.name == "advanced": +# # kwargs["widget"] = JSONEditor() +# # return super().formfield_for_dbfield(db_field, request, **kwargs) +# # +# # def formfield_for_choice_field(self, db_field, request, **kwargs): +# # if db_field.name == "field_type": +# # kwargs["widget"] = Select2Widget() +# # return db_field.formfield(**kwargs) +# # return super().formfield_for_choice_field(db_field, request, **kwargs) +# # +# # def get_changeform_initial_data(self, request): +# # initial = super().get_changeform_initial_data(request) +# # initial.setdefault("advanced", FlexFormField.FLEX_FIELD_DEFAULT_ATTRS) +# # return initial +# # +# # @button(label="editor") +# # def field_editor(self, request, pk): +# # self.editor = FieldEditor(self, request, pk) +# # if request.method == "POST": +# # ret = self.editor.post(request, pk) +# # self.message_user(request, "Saved", messages.SUCCESS) +# # return ret +# # else: +# # return self.editor.get(request, pk) +# # +# # @view() +# # def widget_attrs(self, request, pk): +# # editor = FieldEditor(self, request, pk) +# # return editor.get_configuration() +# # +# # @view() +# # def widget_refresh(self, request, pk): +# # editor = FieldEditor(self, request, pk) +# # return editor.refresh() +# # +# # @view() +# # def widget_code(self, request, pk): +# # editor = FieldEditor(self, request, pk) +# # return editor.get_code() +# # +# # @view() +# # def widget_display(self, request, pk): +# # editor = FieldEditor(self, request, pk) +# # return editor.render() +# # +# # @button() +# # def test(self, request, pk): +# # ctx = self.get_common_context(request, pk) # # try: -# # value = self.used_parameters["registration__exact"] -# # return queryset.filter(Q(registration__exact=value) | Q(formset__parent__registration=value)) -# # except (ValueError, ValidationError) as e: -# # # Fields may raise a ValueError or ValidationError when converting -# # # the parameters to the correct type. -# # raise IncorrectLookupParameters(e) +# # fld = ctx["original"] +# # instance = fld.get_instance() +# # ctx["debug_info"] = { +# # # "widget": getattr(instance, "widget", None), +# # "field_kwargs": fld.get_field_kwargs(), +# # # "options": getattr(instance, "options", None), +# # # "choices": getattr(instance, "choices", None), +# # # "widget_attrs": instance.widget_attrs(instance.widget), +# # } +# # form_class_attrs = { +# # "sample": instance, +# # } +# # form_class = type(forms.Form)("TestForm", (forms.Form,), form_class_attrs) # # +# # if request.method == "POST": +# # form = form_class(request.POST) +# # +# # if form.is_valid(): +# # ctx["debug_info"]["cleaned_data"] = form.cleaned_data +# # self.message_user( +# # request, f"Form validation success. You have selected: {form.cleaned_data['sample']}" +# # ) +# # else: +# # form = form_class() +# # ctx["form"] = form +# # ctx["instance"] = instance +# # except Exception as e: +# # logger.exception(e) +# # ctx["error"] = e +# # raise +# # +# # return render(request, "admin/core/flexformfield/test.html", ctx) # -# class UsedInRFormset(BaseAutoCompleteFilter): -# def has_output(self): -# return "project__exact" in self.request.GET - -# -# @register(FlexForm) -# class FlexFormAdmin(SyncMixin, ConcurrencyVersionAdmin, SmartModelAdmin): -# SYNC_COOKIE = "sync" -# inlines = [ -# FlexFormFieldInline, -# FormSetInline, -# ] -# list_display = ( -# "name", -# # "validator", -# "project", -# "is_main", -# ) -# list_filter = ( -# QueryStringFilter, -# ("project__organization", AutoCompleteFilter), -# ("project", ProjectFilter), -# ("registration", UsedByRegistration), -# ("formset", UsedInRFormset), -# ("formset__parent", UsedInRFormset), -# ) -# search_fields = ("name",) -# readonly_fields = ("version", "last_update_date") -# autocomplete_fields = ("validator", "project") -# ordering = ("name",) -# save_as = True -# -# def get_queryset(self, request): -# return ( -# super() -# .get_queryset(request) -# .prefetch_related("registration_set") -# .select_related( -# "project", -# ) -# ) -# -# def is_main(self, obj): -# return obj.registration_set.exists() -# -# is_main.boolean = True -# -# @button(html_attrs={"class": "aeb-danger"}) -# def invalidate_cache(self, request): -# from ..cache import cache -# -# cache.clear() -# -# @button(label="invalidate cache", html_attrs={"class": "aeb-warn"}) -# def invalidate_cache_single(self, request, pk): -# obj = self.get_object(request, pk) -# obj.save() -# -# @button() -# def inspect(self, request, pk): -# ctx = self.get_common_context(request, pk) -# ctx["title"] = str(ctx["original"]) -# return render(request, "admin/core/flexform/inspect.html", ctx) -# -# @button(label="editor") -# def form_editor(self, request, pk): -# self.editor = FormEditor(self, request, pk) -# if request.method == "POST": -# ret = self.editor.post(request, pk) -# self.message_user(request, "Saved", messages.SUCCESS) -# return ret -# else: -# return self.editor.get(request, pk) -# -# @view() -# def widget_attrs(self, request, pk): -# editor = FormEditor(self, request, pk) -# return editor.get_configuration() -# -# @view() -# def widget_refresh(self, request, pk): -# editor = FormEditor(self, request, pk) -# return editor.refresh() -# -# @view() -# def widget_code(self, request, pk): -# editor = FormEditor(self, request, pk) -# return editor.get_code() -# -# @view() -# def widget_display(self, request, pk): -# editor = FormEditor(self, request, pk) -# return editor.render() -# -# @button() -# def test(self, request, pk): -# ctx = self.get_common_context(request, pk) -# form_class = self.object.get_form_class() -# if request.method == "POST": -# form = form_class(request.POST, initial=self.object.get_initial()) -# if form.is_valid(): -# ctx["cleaned_data"] = form.cleaned_data -# self.message_user(request, "Form is valid") -# else: -# form = form_class(initial=self.object.get_initial()) -# ctx["form"] = form -# return render(request, "admin/core/flexform/test.html", ctx) -# -# # @view(http_basic_auth=True, permission=lambda request, obj: request.user.is_superuser) -# # def export(self, request): -# # try: -# # frm = SyncConfigForm(request.GET) -# # if frm.is_valid(): -# # apps = frm.cleaned_data["apps"] -# # buf = io.StringIO() -# # call_command( -# # "dumpdata", -# # *apps, -# # stdout=buf, -# # exclude=["registration.Record"], -# # use_natural_foreign_keys=True, -# # use_natural_primary_keys=True, -# # ) -# # return JsonResponse(json.loads(buf.getvalue()), safe=False) -# # else: -# # return JsonResponse(frm.errors, status=400) -# # except Exception as e: -# # logger.exception(e) -# # return JsonResponse({}, status=400) -# -# # def _get_signed_cookie(self, request, form): -# # signer = Signer(request.user.password) -# # return signer.sign_object(form.cleaned_data) -# # -# # def _get_saved_credentials(self, request): -# # try: -# # signer = Signer(request.user.password) -# # obj: dict = signer.unsign_object(request.COOKIES.get(self.SYNC_COOKIE, {})) -# # return obj -# # except BadSignature: -# # return {} -# -# # @button(label="Import") -# # def _import(self, request): -# # ctx = self.get_common_context(request, title="Import") -# # cookies = {} -# # if request.method == "POST": -# # form = SyncForm(request.POST) -# # if form.is_valid(): -# # try: -# # auth = HTTPBasicAuth(form.cleaned_data["username"], form.cleaned_data["password"]) -# # if form.cleaned_data["remember"]: -# # cookies = {self.SYNC_COOKIE: self._get_signed_cookie(request, form)} -# # else: -# # cookies = {self.SYNC_COOKIE: ""} -# # url = f"{form.cleaned_data['host']}core/flexform/export/?" -# # for app in form.cleaned_data["apps"]: -# # url += f"apps={app}&" -# # if not url.startswith("http"): -# # url = f"https://{url}" -# # -# # workdir = Path(".").absolute() -# # out = io.StringIO() -# # with requests.get(url, stream=True, auth=auth) as res: -# # if res.status_code != 200: -# # raise Exception(str(res)) -# # ctx["url"] = url -# # with tempfile.NamedTemporaryFile( -# # dir=workdir, prefix="~SYNC", suffix=".json", delete=not settings.DEBUG -# # ) as fdst: -# # fdst.write(res.content) -# # with disable_concurrency(): -# # fixture = (workdir / fdst.name).absolute() -# # call_command("loaddata", fixture, stdout=out, verbosity=3) -# # -# # message = out.getvalue() -# # self.message_user(request, message) -# # ctx["res"] = res -# # except (Exception, JSONDecodeError) as e: -# # logger.exception(e) -# # self.message_error_to_user(request, e) -# # else: -# # form = SyncForm(initial=self._get_saved_credentials(request)) -# # ctx["form"] = form -# # return render(request, "admin/core/flexform/import.html", ctx, cookies=cookies) -# -# -# @register(OptionSet) -# class OptionSetAdmin(LoadDumpMixin, SyncMixin, ConcurrencyVersionAdmin, SmartModelAdmin): -# list_display = ( -# "name", -# "id", -# "separator", -# "comment", -# "pk_col", -# ) -# search_fields = ("name",) -# list_filter = (("data", ValueFilter.factory(lookup_name="icontains")),) -# save_as = True -# readonly_fields = ("version", "last_update_date") -# object_history_template = "reversion-compare/object_history.html" -# -# @button() -# def display_data(self, request, pk): -# ctx = self.get_common_context(request, pk, title="Data") -# obj: OptionSet = ctx["original"] -# data = [] -# for line in obj.data.split("\r\n"): -# data.append(line.split(obj.separator)) -# ctx["data"] = data -# return render(request, "admin/core/optionset/table.html", ctx) +# # +# # class FlexFormFieldInline(LoadDumpMixin, OrderableAdmin, TabularInline): +# # template = "admin/core/flexformfield/tabular.html" +# # model = FlexFormField +# # form = FlexFormFieldFormInline +# # fields = ("ordering", "label", "name", "required", "enabled", "field_type") +# # show_change_link = True +# # extra = 0 +# # ordering_field = "ordering" +# # ordering_field_hide_input = True +# # +# # def formfield_for_choice_field(self, db_field, request, **kwargs): +# # # if db_field.name == "field_type": +# # # kwargs["widget"] = Select2Widget() +# # # return db_field.formfield(**kwargs) +# # return super().formfield_for_choice_field(db_field, request, **kwargs) +# # # -# @link(change_form=True, change_list=False, html_attrs={"target": "_new"}) -# def view_json(self, button): -# original = button.context["original"] -# if original: -# try: -# button.href = original.get_api_url() -# except NoReverseMatch: -# button.href = "#" -# button.label = "Error reversing url" # -# def change_view(self, request, object_id, form_url="", extra_context=None): -# if request.method == "POST" and "_saveasnew" in request.POST: -# object_id = None +# class SyncConfigForm(forms.Form): +# APPS = ("core", "registration") +# apps = forms.MultipleChoiceField(choices=zip(APPS, APPS), widget=forms.CheckboxSelectMultiple()) # -# return super().change_view(request, object_id) # +# class SyncForm(SyncConfigForm): +# host = forms.CharField() +# username = forms.CharField() +# password = forms.CharField(widget=forms.PasswordInput) +# remember = forms.BooleanField(label="Remember me", required=False) # -# @register(CustomFieldType) -# class CustomFieldTypeAdmin(SmartModelAdmin): -# list_display = ( -# "name", -# "base_type", -# "attrs", -# ) -# search_fields = ("name",) -# formfield_overrides = { -# JSONField: {"widget": JSONEditor}, -# } # -# @button() -# def test(self, request, pk): -# ctx = self.get_common_context(request, pk) -# fld = ctx["original"] -# field_type = fld.base_type -# kwargs = fld.attrs.copy() -# field = field_type(**kwargs) -# form_class_attrs = { -# "sample": field, -# } -# formClass = type(forms.Form)("TestForm", (forms.Form,), form_class_attrs) +# # +# # class ProjectFilter(AutoCompleteFilter): +# # fk_name = "project__organization__exact" +# # +# # def __init__(self, field, request, params, model, model_admin, field_path): +# # self.request = request +# # super().__init__(field, request, params, model, model_admin, field_path) +# # +# # def has_output(self): +# # return "project__organization__exact" in self.request.GET +# # +# # def get_url(self): +# # url = reverse("%s:autocomplete" % self.admin_site.name) +# # if self.fk_name in self.request.GET: +# # oid = self.request.GET[self.fk_name] +# # return f"{url}?oid={oid}" +# # return url +# # +# # # +# # # class UsedByRegistration(BaseAutoCompleteFilter): +# # # def has_output(self): +# # # return "project__exact" in self.request.GET +# # # +# # # def queryset(self, request, queryset): +# # # # {'registration__exact': '30'} +# # # if not self.used_parameters: +# # # return queryset +# # # try: +# # # value = self.used_parameters["registration__exact"] +# # # return queryset.filter(Q(registration__exact=value) | Q(formset__parent__registration=value)) +# # # except (ValueError, ValidationError) as e: +# # # # Fields may raise a ValueError or ValidationError when converting +# # # # the parameters to the correct type. +# # # raise IncorrectLookupParameters(e) +# # # +# # +# # class UsedInRFormset(BaseAutoCompleteFilter): +# # def has_output(self): +# # return "project__exact" in self.request.GET # -# if request.method == "POST": -# form = formClass(request.POST) -# if form.is_valid(): -# self.message_user( -# request, f"Form validation success. " f"You have selected: {form.cleaned_data['sample']}" -# ) -# else: -# form = formClass() -# ctx["form"] = form -# return render(request, "admin/core/customfieldtype/test.html", ctx) +# # +# # @register(FlexForm) +# # class FlexFormAdmin(SyncMixin, ConcurrencyVersionAdmin, SmartModelAdmin): +# # SYNC_COOKIE = "sync" +# # inlines = [ +# # FlexFormFieldInline, +# # FormSetInline, +# # ] +# # list_display = ( +# # "name", +# # # "validator", +# # "project", +# # "is_main", +# # ) +# # list_filter = ( +# # QueryStringFilter, +# # ("project__organization", AutoCompleteFilter), +# # ("project", ProjectFilter), +# # ("registration", UsedByRegistration), +# # ("formset", UsedInRFormset), +# # ("formset__parent", UsedInRFormset), +# # ) +# # search_fields = ("name",) +# # readonly_fields = ("version", "last_update_date") +# # autocomplete_fields = ("validator", "project") +# # ordering = ("name",) +# # save_as = True +# # +# # def get_queryset(self, request): +# # return ( +# # super() +# # .get_queryset(request) +# # .prefetch_related("registration_set") +# # .select_related( +# # "project", +# # ) +# # ) +# # +# # def is_main(self, obj): +# # return obj.registration_set.exists() +# # +# # is_main.boolean = True +# # +# # @button(html_attrs={"class": "aeb-danger"}) +# # def invalidate_cache(self, request): +# # from ..cache import cache +# # +# # cache.clear() +# # +# # @button(label="invalidate cache", html_attrs={"class": "aeb-warn"}) +# # def invalidate_cache_single(self, request, pk): +# # obj = self.get_object(request, pk) +# # obj.save() +# # +# # @button() +# # def inspect(self, request, pk): +# # ctx = self.get_common_context(request, pk) +# # ctx["title"] = str(ctx["original"]) +# # return render(request, "admin/core/flexform/inspect.html", ctx) +# # +# # @button(label="editor") +# # def form_editor(self, request, pk): +# # self.editor = FormEditor(self, request, pk) +# # if request.method == "POST": +# # ret = self.editor.post(request, pk) +# # self.message_user(request, "Saved", messages.SUCCESS) +# # return ret +# # else: +# # return self.editor.get(request, pk) +# # +# # @view() +# # def widget_attrs(self, request, pk): +# # editor = FormEditor(self, request, pk) +# # return editor.get_configuration() +# # +# # @view() +# # def widget_refresh(self, request, pk): +# # editor = FormEditor(self, request, pk) +# # return editor.refresh() +# # +# # @view() +# # def widget_code(self, request, pk): +# # editor = FormEditor(self, request, pk) +# # return editor.get_code() +# # +# # @view() +# # def widget_display(self, request, pk): +# # editor = FormEditor(self, request, pk) +# # return editor.render() +# # +# # @button() +# # def test(self, request, pk): +# # ctx = self.get_common_context(request, pk) +# # form_class = self.object.get_form_class() +# # if request.method == "POST": +# # form = form_class(request.POST, initial=self.object.get_initial()) +# # if form.is_valid(): +# # ctx["cleaned_data"] = form.cleaned_data +# # self.message_user(request, "Form is valid") +# # else: +# # form = form_class(initial=self.object.get_initial()) +# # ctx["form"] = form +# # return render(request, "admin/core/flexform/test.html", ctx) +# # +# # # @view(http_basic_auth=True, permission=lambda request, obj: request.user.is_superuser) +# # # def export(self, request): +# # # try: +# # # frm = SyncConfigForm(request.GET) +# # # if frm.is_valid(): +# # # apps = frm.cleaned_data["apps"] +# # # buf = io.StringIO() +# # # call_command( +# # # "dumpdata", +# # # *apps, +# # # stdout=buf, +# # # exclude=["registration.Record"], +# # # use_natural_foreign_keys=True, +# # # use_natural_primary_keys=True, +# # # ) +# # # return JsonResponse(json.loads(buf.getvalue()), safe=False) +# # # else: +# # # return JsonResponse(frm.errors, status=400) +# # # except Exception as e: +# # # logger.exception(e) +# # # return JsonResponse({}, status=400) +# # +# # # def _get_signed_cookie(self, request, form): +# # # signer = Signer(request.user.password) +# # # return signer.sign_object(form.cleaned_data) +# # # +# # # def _get_saved_credentials(self, request): +# # # try: +# # # signer = Signer(request.user.password) +# # # obj: dict = signer.unsign_object(request.COOKIES.get(self.SYNC_COOKIE, {})) +# # # return obj +# # # except BadSignature: +# # # return {} +# # +# # # @button(label="Import") +# # # def _import(self, request): +# # # ctx = self.get_common_context(request, title="Import") +# # # cookies = {} +# # # if request.method == "POST": +# # # form = SyncForm(request.POST) +# # # if form.is_valid(): +# # # try: +# # # auth = HTTPBasicAuth(form.cleaned_data["username"], form.cleaned_data["password"]) +# # # if form.cleaned_data["remember"]: +# # # cookies = {self.SYNC_COOKIE: self._get_signed_cookie(request, form)} +# # # else: +# # # cookies = {self.SYNC_COOKIE: ""} +# # # url = f"{form.cleaned_data['host']}core/flexform/export/?" +# # # for app in form.cleaned_data["apps"]: +# # # url += f"apps={app}&" +# # # if not url.startswith("http"): +# # # url = f"https://{url}" +# # # +# # # workdir = Path(".").absolute() +# # # out = io.StringIO() +# # # with requests.get(url, stream=True, auth=auth) as res: +# # # if res.status_code != 200: +# # # raise Exception(str(res)) +# # # ctx["url"] = url +# # # with tempfile.NamedTemporaryFile( +# # # dir=workdir, prefix="~SYNC", suffix=".json", delete=not settings.DEBUG +# # # ) as fdst: +# # # fdst.write(res.content) +# # # with disable_concurrency(): +# # # fixture = (workdir / fdst.name).absolute() +# # # call_command("loaddata", fixture, stdout=out, verbosity=3) +# # # +# # # message = out.getvalue() +# # # self.message_user(request, message) +# # # ctx["res"] = res +# # # except (Exception, JSONDecodeError) as e: +# # # logger.exception(e) +# # # self.message_error_to_user(request, e) +# # # else: +# # # form = SyncForm(initial=self._get_saved_credentials(request)) +# # # ctx["form"] = form +# # # return render(request, "admin/core/flexform/import.html", ctx, cookies=cookies) +# # +# # +# # @register(OptionSet) +# # class OptionSetAdmin(LoadDumpMixin, SyncMixin, ConcurrencyVersionAdmin, SmartModelAdmin): +# # list_display = ( +# # "name", +# # "id", +# # "separator", +# # "comment", +# # "pk_col", +# # ) +# # search_fields = ("name",) +# # list_filter = (("data", ValueFilter.factory(lookup_name="icontains")),) +# # save_as = True +# # readonly_fields = ("version", "last_update_date") +# # object_history_template = "reversion-compare/object_history.html" +# # +# # @button() +# # def display_data(self, request, pk): +# # ctx = self.get_common_context(request, pk, title="Data") +# # obj: OptionSet = ctx["original"] +# # data = [] +# # for line in obj.data.split("\r\n"): +# # data.append(line.split(obj.separator)) +# # ctx["data"] = data +# # return render(request, "admin/core/optionset/table.html", ctx) +# # +# # @link(change_form=True, change_list=False, html_attrs={"target": "_new"}) +# # def view_json(self, button): +# # original = button.context["original"] +# # if original: +# # try: +# # button.href = original.get_api_url() +# # except NoReverseMatch: +# # button.href = "#" +# # button.label = "Error reversing url" +# # +# # def change_view(self, request, object_id, form_url="", extra_context=None): +# # if request.method == "POST" and "_saveasnew" in request.POST: +# # object_id = None +# # +# # return super().change_view(request, object_id) +# # +# # +# # @register(CustomFieldType) +# # class CustomFieldTypeAdmin(SmartModelAdmin): +# # list_display = ( +# # "name", +# # "base_type", +# # "attrs", +# # ) +# # search_fields = ("name",) +# # formfield_overrides = { +# # JSONField: {"widget": JSONEditor}, +# # } +# # +# # @button() +# # def test(self, request, pk): +# # ctx = self.get_common_context(request, pk) +# # fld = ctx["original"] +# # field_type = fld.base_type +# # kwargs = fld.attrs.copy() +# # field = field_type(**kwargs) +# # form_class_attrs = { +# # "sample": field, +# # } +# # formClass = type(forms.Form)("TestForm", (forms.Form,), form_class_attrs) +# # +# # if request.method == "POST": +# # form = formClass(request.POST) +# # if form.is_valid(): +# # self.message_user( +# # request, f"Form validation success. " f"You have selected: {form.cleaned_data['sample']}" +# # ) +# # else: +# # form = formClass() +# # ctx["form"] = form +# # return render(request, "admin/core/customfieldtype/test.html", ctx) diff --git a/src/aurora/core/admin/project.py b/src/aurora/core/admin/project.py index 365a44df..77389531 100644 --- a/src/aurora/core/admin/project.py +++ b/src/aurora/core/admin/project.py @@ -16,7 +16,7 @@ @register(Project) class ProjectAdmin(SyncMixin, LinkedObjectsMixin, MPTTModelAdmin): - list_display = ("name",) + list_display = ("name", "organization") list_filter = ("organization",) mptt_level_indent = 20 mptt_indent_field = "name" @@ -24,6 +24,9 @@ class ProjectAdmin(SyncMixin, LinkedObjectsMixin, MPTTModelAdmin): protocol_class = AuroraSyncProjectProtocol autocomplete_fields = "parent, " + def get_queryset(self, request): + return super().get_queryset(request).select_related("organization") + def get_search_results(self, request, queryset, search_term): queryset, may_have_duplicates = super().get_search_results(request, queryset, search_term) if "oid" in request.GET: diff --git a/src/aurora/core/backends.py b/src/aurora/core/backends.py index 4bfe6c72..d3818bcf 100644 --- a/src/aurora/core/backends.py +++ b/src/aurora/core/backends.py @@ -6,16 +6,24 @@ User = get_user_model() -class AnyUserAuthBackend(ModelBackend): +class AnyUserAuthBackend(ModelBackend): # pragma: no cover + # Develop only backend def authenticate(self, request, username=None, password=None, **kwargs): host = request.get_host() if settings.DEBUG and (host.startswith("localhost") or host.startswith("127.0.0.1")): try: - user, __ = User.objects.update_or_create( - username=username, is_staff=True, is_active=True, is_superuser=True, email=f"{username}@demo.org" - ) - user.set_password(password) - user.save() - return user + if username.startswith("admin"): + values = dict(is_staff=True, is_active=True, is_superuser=True) + elif username.startswith("user"): + values = dict(is_staff=False, is_active=True, is_superuser=False) + else: + values = dict() + if values: + user, __ = User.objects.update_or_create( + username=username, defaults={"email": f"{username}@demo.org", **values} + ) + user.set_password(password) + user.save() + return user except (User.DoesNotExist, IntegrityError): pass diff --git a/src/aurora/core/channels.py b/src/aurora/core/channels.py index 9ab1a72f..fe202cd9 100644 --- a/src/aurora/core/channels.py +++ b/src/aurora/core/channels.py @@ -1,45 +1,45 @@ -import json -from collections import defaultdict -from urllib import parse - -from channels.generic.websocket import WebsocketConsumer -from django.core.cache import caches -from django.urls import path - -cache = caches["default"] - - -class FieldEditorConsumer(WebsocketConsumer): - def connect(self): - self.accept() - - def disconnect(self, close_code): - pass - - def receive(self, text_data=None, bytes_data=None): - data = dict(parse.parse_qsl(text_data)) - data.pop("csrfmiddlewaretoken") - config = defaultdict(dict) - for name, value in data.items(): - prefix, field_name = name.split("-") - config[prefix][field_name] = value - cache.set(self.scope["path"], config) - self.send(text_data=json.dumps({"message": data})) - - -class FieldWidgetConsumer(WebsocketConsumer): - def connect(self): - self.accept() - - def disconnect(self, close_code): - pass - - def receive(self, text_data=None, bytes_data=None): - data = parse.parse_qs(text_data) - self.send(text_data=json.dumps({"message": data})) - - -websocket_urlpatterns = [ - path("editor/field///", FieldEditorConsumer.as_asgi()), - path("widget/field///", FieldWidgetConsumer.as_asgi()), -] +# import json +# from collections import defaultdict +# from urllib import parse +# +# from channels.generic.websocket import WebsocketConsumer +# from django.core.cache import caches +# from django.urls import path +# +# cache = caches["default"] +# +# +# class FieldEditorConsumer(WebsocketConsumer): +# def connect(self): +# self.accept() +# +# def disconnect(self, close_code): +# pass +# +# def receive(self, text_data=None, bytes_data=None): +# data = dict(parse.parse_qsl(text_data)) +# data.pop("csrfmiddlewaretoken") +# config = defaultdict(dict) +# for name, value in data.items(): +# prefix, field_name = name.split("-") +# config[prefix][field_name] = value +# cache.set(self.scope["path"], config) +# self.send(text_data=json.dumps({"message": data})) +# +# +# class FieldWidgetConsumer(WebsocketConsumer): +# def connect(self): +# self.accept() +# +# def disconnect(self, close_code): +# pass +# +# def receive(self, text_data=None, bytes_data=None): +# data = parse.parse_qs(text_data) +# self.send(text_data=json.dumps({"message": data})) +# +# +# websocket_urlpatterns = [ +# path("editor/field///", FieldEditorConsumer.as_asgi()), +# path("widget/field///", FieldWidgetConsumer.as_asgi()), +# ] diff --git a/src/aurora/core/fields/select.py b/src/aurora/core/fields/select.py index e5a84293..421eab9d 100644 --- a/src/aurora/core/fields/select.py +++ b/src/aurora/core/fields/select.py @@ -6,8 +6,8 @@ from django.urls import NoReverseMatch, reverse from django.utils.translation import get_language -from .widgets.mixins import TailWindMixin from ..version_media import VersionMedia +from .widgets.mixins import TailWindMixin logger = logging.getLogger(__name__) diff --git a/src/aurora/core/fields/widgets/datetime.py b/src/aurora/core/fields/widgets/datetime.py index 4ba10a1e..87549c4b 100644 --- a/src/aurora/core/fields/widgets/datetime.py +++ b/src/aurora/core/fields/widgets/datetime.py @@ -1,8 +1,8 @@ from django import forms from django.conf import settings -from .mixins import TailWindMixin from ...version_media import VersionMedia +from .mixins import TailWindMixin class SmartDateWidget(TailWindMixin, forms.DateInput): diff --git a/src/aurora/core/migrations/0054_auto_20231123_0605.py b/src/aurora/core/migrations/0054_auto_20231123_0605.py new file mode 100644 index 00000000..2c5c2ad3 --- /dev/null +++ b/src/aurora/core/migrations/0054_auto_20231123_0605.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.18 on 2023-11-23 06:05 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0053_auto_20230321_0604"), + ] + + operations = [ + migrations.AlterField( + model_name="flexform", + name="project", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="core.project"), + ), + migrations.AlterField( + model_name="project", + name="organization", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="projects", to="core.organization" + ), + ), + ] diff --git a/src/aurora/core/migrations/0055_alter_project_slug.py b/src/aurora/core/migrations/0055_alter_project_slug.py new file mode 100644 index 00000000..56196105 --- /dev/null +++ b/src/aurora/core/migrations/0055_alter_project_slug.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.18 on 2023-11-23 06:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0054_auto_20231123_0605"), + ] + + operations = [ + migrations.AlterField( + model_name="project", + name="slug", + field=models.SlugField(blank=True, max_length=100), + ), + ] diff --git a/src/aurora/core/models.py b/src/aurora/core/models.py index 96c17497..6aea2725 100644 --- a/src/aurora/core/models.py +++ b/src/aurora/core/models.py @@ -4,14 +4,13 @@ from datetime import date, datetime, time from inspect import isclass from json import JSONDecodeError - -from django.contrib.admin.templatetags.admin_urls import admin_urlname from pathlib import Path import jsonpickle from admin_ordering.models import OrderableModel from concurrency.fields import AutoIncVersionField from django import forms +from django.contrib.admin.templatetags.admin_urls import admin_urlname from django.contrib.postgres.fields import CICharField from django.core.cache import caches from django.core.exceptions import ValidationError @@ -55,12 +54,14 @@ def get_admin_changelist_url(self): return reverse(admin_urlname(self._meta, "changelist")) -class OrganizationManager(TreeManager): +class OrganizationManager(NaturalKeyModelManager, TreeManager): def get_by_natural_key(self, slug): return self.get(slug=slug) -class Organization(AdminReverseMixin, MPTTModel): +class Organization(AdminReverseMixin, NaturalKeyModel, MPTTModel): + _natural_key = ("slug",) + version = AutoIncVersionField() last_update_date = models.DateTimeField(auto_now=True) @@ -76,27 +77,26 @@ class MPTTMeta: def __str__(self): return self.name - def natural_key(self): - return (self.slug,) - def save(self, *args, **kwargs): if self._state.adding and not self.slug: self.slug = slugify(self.name) super().save(*args, **kwargs) -class ProjectManager(TreeManager): +class ProjectManager(NaturalKeyModelManager, TreeManager): def get_by_natural_key(self, slug, org_slug): return self.get(slug=slug, organization__slug=org_slug) -class Project(AdminReverseMixin, MPTTModel): +class Project(AdminReverseMixin, NaturalKeyModel, MPTTModel): + _natural_key = ("slug", "organization") + version = AutoIncVersionField() last_update_date = models.DateTimeField(auto_now=True) name = CICharField(max_length=100, unique=True) - slug = models.SlugField(max_length=100, blank=True, null=True) - organization = models.ForeignKey(Organization, null=True, related_name="projects", on_delete=models.CASCADE) + slug = models.SlugField(max_length=100, blank=True) + organization = models.ForeignKey(Organization, related_name="projects", on_delete=models.CASCADE) parent = TreeForeignKey("self", on_delete=models.CASCADE, null=True, blank=True, related_name="children") objects = ProjectManager() @@ -110,9 +110,6 @@ class Meta: def __str__(self): return self.name - def natural_key(self): - return self.slug, self.organization.slug - def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name) @@ -304,7 +301,7 @@ def inner(value): class FlexForm(AdminReverseMixin, I18NModel, NaturalKeyModel): version = AutoIncVersionField() last_update_date = models.DateTimeField(auto_now=True) - project = models.ForeignKey(Project, null=True, on_delete=models.CASCADE) + project = models.ForeignKey(Project, on_delete=models.CASCADE) name = CICharField(max_length=255, unique=True) base_type = StrategyClassField(registry=form_registry, default=FlexFormBaseForm) validator = models.ForeignKey( diff --git a/src/aurora/core/templates/smart/_form.html b/src/aurora/core/templates/smart/_form.html index 0be3c4e2..2a2c408d 100644 --- a/src/aurora/core/templates/smart/_form.html +++ b/src/aurora/core/templates/smart/_form.html @@ -1,4 +1,4 @@ -{% load aurora formset l10n static itrans http2 %} +{% load aurora formset l10n static itrans %}