From cd8621e74293c84c412e2b44df77c01423895825 Mon Sep 17 00:00:00 2001 From: Daniel Rabstejnek Date: Mon, 27 Jan 2025 16:23:25 -0500 Subject: [PATCH] Cleanup --- hawc/apps/common/forms.py | 3 + hawc/apps/common/widgets.py | 25 +++-- hawc/apps/udf/forms.py | 181 +++++------------------------------- hawc/apps/udf/views.py | 10 +- hawc/constants.py | 1 + 5 files changed, 54 insertions(+), 166 deletions(-) diff --git a/hawc/apps/common/forms.py b/hawc/apps/common/forms.py index cdce7bee58..5bd79e8408 100644 --- a/hawc/apps/common/forms.py +++ b/hawc/apps/common/forms.py @@ -459,6 +459,9 @@ def validate(self, value): if not form.is_valid(): raise forms.ValidationError(self.error_messages["invalid"]) +class NewDynamicFormField(DynamicFormField): + widget = widgets.NewDynamicFormWidget + class InlineRadioChoiceField(forms.ChoiceField): """Choice widget that uses radio buttons that are inline.""" diff --git a/hawc/apps/common/widgets.py b/hawc/apps/common/widgets.py index 460d003947..bb34e01c52 100644 --- a/hawc/apps/common/widgets.py +++ b/hawc/apps/common/widgets.py @@ -148,25 +148,36 @@ def add_prefix(self, field_name): def format_value(self, value): """Value used in rendering.""" - #import pdb; pdb.set_trace() if value is not None and not isinstance(value,dict): value = json.loads(value) if value: value = {self.add_prefix(k): v for k, v in value.items()} return self.form_class(data=value, **self.form_kwargs) + def value_from_datadict(self, data, files, name): + """Parse value from POST request.""" + self.form_kwargs.pop("prefix",None) + form = self.form_class(data=data,prefix=name, **self.form_kwargs) + form.full_clean() + return form.cleaned_data + + class Media: + js = ["js/udf.js"] + +class NewDynamicFormWidget(DynamicFormWidget): + """Dynamic form widget that uses name instead of prefix.""" def get_context(self, name, value, attrs): if value is not None and not isinstance(value,dict): value = json.loads(value) if value: value = {f"{name}-{k}": v for k, v in value.items()} - foobar= self.form_class(data=value, prefix=name) + formatted_value = self.form_class(data=value, prefix=name) return { "widget": { "name": name, "is_hidden": self.is_hidden, "required": self.is_required, - "value": foobar, + "value": formatted_value, "attrs": self.build_attrs(self.attrs, attrs), "template_name": self.template_name, }, @@ -174,14 +185,10 @@ def get_context(self, name, value, attrs): def value_from_datadict(self, data, files, name): """Parse value from POST request.""" - form = self.form_class(data=data, **self.form_kwargs) + self.form_kwargs.pop("prefix",None) + form = self.form_class(data=data,prefix=name, **self.form_kwargs) form.full_clean() - #import pdb; pdb.set_trace() return form.cleaned_data - class Media: - js = ["js/udf.js"] - - class ColorInput(TextInput): input_type = "color" diff --git a/hawc/apps/udf/forms.py b/hawc/apps/udf/forms.py index 241ad3a324..0d7a3e45c2 100644 --- a/hawc/apps/udf/forms.py +++ b/hawc/apps/udf/forms.py @@ -11,60 +11,17 @@ from ..assessment.models import Assessment from ..common.autocomplete.forms import AutocompleteSelectMultipleWidget from ..common.dynamic_forms.schemas import Schema -from ..common.forms import BaseFormHelper, PydanticValidator, form_actions_big, DynamicFormField +from ..common.forms import BaseFormHelper, PydanticValidator, form_actions_big, NewDynamicFormField from ..common.helper import get_current_user from ..common.views import create_object_log from ..lit.models import ReferenceFilterTag from ..myuser.autocomplete import UserAutocomplete from . import cache, constants, models -class FormAsField(forms.Field): - # render a form as a field, something like a fieldset - ### probably use DynamicFormField - pass - -class ArrayWidgetOld(forms.Widget): - template_name = "udf/widget.html" - - class Media: - js = (f"{settings.STATIC_URL}js/array.js",) - -class ArrayFieldOld(forms.Field): - # on render have the underlying field disabled & hidden. this will be copy & pasted on "create" click - # add [] suffix to fields here so that they're POSTed as arrays (AlpineJS ideally, likely jQuery) - # create & delete element buttons - # arrow buttons for reordering - widget = ArrayWidgetOld - - def __init__(self,field): - self.field = field - super().__init__() - - def widget_attrs(self, widget): - attrs = super().widget_attrs(widget) - if isinstance(widget, ArrayWidgetOld) and "field" not in widget.attrs: - attrs.setdefault("field", self.field) - return attrs - -class SchemaFieldForm(forms.Form): - # use dynamic form w/ conditional? since we want different forms - # depending on what field type is chosen - # conditional logic may clash with js from array field (ie suffixes added to names) - # may have to bring over HERO AlpineJS implementation of conditional logic to make it work - pass - class ArrayWidget(forms.MultiWidget): - # TODO: override template to create hidden input, - # "Add" button that grabs hidden input and appends it, - # "Delete" button that removes field - # Ideally add & delete with alpineJS, if not then jQuery most likely template_name = "udf/widget.html" - #class Media: - # may not be necessary if done entirely with alpineJS - #js = (f"{settings.STATIC_URL}js/array.js",) - def __init__(self, default_widget, attrs=None): super(forms.MultiWidget,self).__init__(attrs) self.default_widget = default_widget @@ -87,26 +44,20 @@ def initialize_widgets_from_fields(self, fields:list): self.initialize_widgets(widgets) def value_from_datadict(self, data, files, name): - #import pdb; pdb.set_trace() try: getter = data.getlist except AttributeError: getter = data.get - # TODO replace this logic with index based logic; - # should look cleaner here, but trickier on frontend keys = [_ for _ in data.keys() if _.startswith(name+self.suffix)] lists = [getter(key) for key in keys] values = [{(name + k.removeprefix(name+self.suffix)):v[i] for i,k in enumerate(keys)} for v in zip(*lists)] - if values: - foo = [ + return [ self.default_widget.value_from_datadict(value, files, name) for value in values ] - #import pdb; pdb.set_trace() - return foo return getter(name) or [] @@ -153,7 +104,6 @@ def initialize_fields(self, fields:list): self.widget.initialize_widgets_from_fields(self.fields) def initialize_fields_from_value(self, value:list): - #import pdb; pdb.set_trace() fields = [deepcopy(self.field) for _ in value] self.initialize_fields(fields) @@ -221,23 +171,14 @@ def helper(self): return helper -class Foobar(forms.Form): - fields = ArrayField(DynamicFormField(prefix="schema_builder-fields",form_class=FieldForm)) - #conditions = ArrayField(DynamicFormField(required=False,prefix="schema_builder-conditions",form_class=ConditionForm)) +class SchemaForm(forms.Form): + fields = ArrayField(NewDynamicFormField(prefix="schema_builder-fields",form_class=FieldForm)) + #conditions = ArrayField(NewDynamicFormField(required=False,prefix="schema_builder-conditions",form_class=ConditionForm)) def __init__(self,*args,**kwargs): - #kwargs.pop("instance") - #kwargs.pop("user") - #kwargs["data"] = {"fields":[1,2,3]} super().__init__(*args,**kwargs) - #import pdb; pdb.set_trace() - #self.fields["fields"].initialize_fields_from_value(kwargs.get("data",{}).get("fields",[])) - #self.fields["conditions"].initialize_fields_from_value(kwargs.get("data",{}).get("conditions",[])) - #import pdb; pdb.set_trace() self.fields["fields"].initialize_fields_from_value(self.fields["fields"].widget.value_from_datadict(self.data, None, 'schema_builder-fields') or []) - #self.fields["conditions"].initialize_fields_from_value(self.fields["conditions"].widget.value_from_datadict(self.data, None, "conditions") or []) - #import pdb; pdb.set_trace() - return + #self.fields["conditions"].initialize_fields_from_value(self.fields["conditions"].widget.value_from_datadict(self.data, None, "schema_builder-conditions") or []) @property @@ -246,13 +187,8 @@ def helper(self): helper.form_tag = False return helper -class UDFForm(forms.ModelForm): - schema_builder = DynamicFormField( - prefix="schema_builder", - form_class=Foobar, - validators=[PydanticValidator(Schema)],) - +class UDFForm(forms.ModelForm): schema = forms.JSONField( initial=Schema(fields=[]).model_dump(), validators=[PydanticValidator(Schema)], @@ -261,8 +197,6 @@ class UDFForm(forms.ModelForm): def __init__(self, *args, **kwargs): user = kwargs.pop("user") super().__init__(*args, **kwargs) - if schema:=self.initial.get("schema"): - self.initial["schema_builder"] = schema if self.instance.id is None: self.instance.creator = user @@ -302,16 +236,6 @@ def clean_name(self): raise forms.ValidationError("The UDF name must be unique for your account.") return name - def clean(self): - #import pdb; pdb.set_trace() - # remove errors if schema has manually been changed - cleaned_data = super().clean() - if "schema" in self.changed_data: - self._errors.pop("schema_builder",None) - elif "schema_builder" in cleaned_data: - cleaned_data["schema"] = cleaned_data["schema_builder"] - return cleaned_data - @transaction.atomic def save(self, commit=True): verb = "Created" if self.instance.id is None else "Updated" @@ -363,9 +287,6 @@ def helper(self): ), cfl.Fieldset( "Form Schema", - cfl.Row( - cfl.Column("schema_builder"), - ), cfl.Row( cfl.Column("schema"), ), @@ -383,81 +304,28 @@ def helper(self): return helper - - - - - -class UDFForm1(forms.ModelForm): - schema = forms.JSONField( - initial=Schema(fields=[]).model_dump(), - validators=[PydanticValidator(Schema)], - ) +class NewUDFForm(UDFForm): + schema_builder = NewDynamicFormField( + prefix="schema_builder", + form_class=SchemaForm, + validators=[PydanticValidator(Schema)],) def __init__(self, *args, **kwargs): - user = kwargs.pop("user") super().__init__(*args, **kwargs) - if self.instance.id is None: - self.instance.creator = user - - # filter assessment list to a subset of those available - self.fields["assessments"].queryset = ( - Assessment.objects.all().user_can_view(self.instance.creator).order_by("name") - ) - self.fields[ - "assessments" - ].help_text += ( - f" Only assessments the creator ({self.instance.creator}) can view are shown." - ) + if schema:=self.initial.get("schema"): + self.initial["schema_builder"] = schema - self.fields["schema"].label_append_button = { - "btn_attrs": { - "hx-indicator": "#spinner", - "id": "schema-preview-btn", - "hx-post": reverse_lazy("udf:schema_preview"), - "hx-target": "#schema-preview-fieldset", - "hx-swap": "innerHTML", - "class": "ml-2 btn btn-primary", - }, - "btn_content": "Preview", - } - self.fields["schema"].help_text = ( - models.UserDefinedForm.schema.field.help_text - + " Load an example." - ) - def clean_name(self): - # check unique_together ("creator", "name") - name = self.cleaned_data.get("name") - if ( - name != self.instance.name - and self._meta.model.objects.filter(creator=self.instance.creator, name=name).exists() - ): - raise forms.ValidationError("The UDF name must be unique for your account.") - return name + def clean(self): + # remove errors if schema has manually been changed + cleaned_data = super().clean() + if "schema" in self.changed_data: + self._errors.pop("schema_builder",None) + elif "schema_builder" in cleaned_data: + cleaned_data["schema"] = cleaned_data["schema_builder"] + return cleaned_data - @transaction.atomic - def save(self, commit=True): - verb = "Created" if self.instance.id is None else "Updated" - instance = super().save(commit=commit) - if commit: - create_object_log(verb, instance, None, self.instance.creator_id, "") - return instance - class Meta: - model = models.UserDefinedForm - fields = ( - "name", - "description", - "schema", - "editors", - "assessments", - "published", - "deprecated", - ) - widgets = { - "editors": AutocompleteSelectMultipleWidget(UserAutocomplete), - } @property def helper(self): @@ -487,6 +355,9 @@ def helper(self): ), cfl.Fieldset( "Form Schema", + cfl.Row( + cfl.Column("schema_builder"), + ), cfl.Row( cfl.Column("schema"), ), @@ -503,8 +374,6 @@ def helper(self): ) return helper - - class SchemaPreviewForm(forms.Form): """Form for previewing a Dynamic Form schema.""" diff --git a/hawc/apps/udf/views.py b/hawc/apps/udf/views.py index e01a213d3a..b057324c5a 100644 --- a/hawc/apps/udf/views.py +++ b/hawc/apps/udf/views.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.core.exceptions import BadRequest, PermissionDenied from django.db.models.query import QuerySet from django.http import HttpRequest @@ -56,10 +57,13 @@ class CreateUDFView(LoginRequiredMixin, MessageMixin, CreateView): success_url = reverse_lazy("udf:udf_list") success_message = "Form created." + def get_form_class(self): + """Return the form class to use in this view.""" + return forms.NewUDFForm if settings.HAWC_FEATURES.ENABLE_SCHEMA_BUILDER else forms.UDFForm + def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs.update(user=self.request.user) - #kwargs.update(data={"fields":["a","b","c"]}) return kwargs @@ -70,6 +74,10 @@ class UpdateUDFView(MessageMixin, UpdateView): success_url = reverse_lazy("udf:udf_list") success_message = "Form updated." + def get_form_class(self): + """Return the form class to use in this view.""" + return forms.NewUDFForm if settings.HAWC_FEATURES.ENABLE_SCHEMA_BUILDER else forms.UDFForm + def get_object(self, **kw): obj = super().get_object(**kw) if not obj.user_can_edit(self.request.user): diff --git a/hawc/constants.py b/hawc/constants.py index 550e8c0a91..6b4e9c30fc 100644 --- a/hawc/constants.py +++ b/hawc/constants.py @@ -20,6 +20,7 @@ class FeatureFlags(BaseModel): ENABLE_FILTER_DOWNLOADS: bool = False ENABLE_DOCS_LINK: bool = False ENABLE_NEW_HERO: bool = False + ENABLE_SCHEMA_BUILDER: bool = False @classmethod def from_env(cls, variable) -> "FeatureFlags":