[ADD] DYNAMIC FIELDS: Add new app - dynamic fields

This commit is contained in:
Horilla
2025-01-06 16:22:16 +05:30
parent 6b2ccf66ab
commit d6ab7f5d74
23 changed files with 1004 additions and 0 deletions

View File

8
dynamic_fields/admin.py Normal file
View File

@@ -0,0 +1,8 @@
from django.contrib import admin
# Register your models here.
from dynamic_fields.models import Choice, DynamicField
admin.site.register(DynamicField)
admin.site.register(Choice)

62
dynamic_fields/apps.py Normal file
View File

@@ -0,0 +1,62 @@
import logging
import sys
from django.apps import AppConfig
from django.db import connection
from django.core.management import call_command
from dynamic_fields.methods import column_exists
logger = logging.getLogger(__name__)
class DynamicFieldsConfig(AppConfig):
"""
DynamicFieldsConfig
"""
default_auto_field = "django.db.models.BigAutoField"
name = "dynamic_fields"
def ready(self):
from dynamic_fields.models import DynamicField
from django.contrib.contenttypes.models import ContentType
from simple_history.models import HistoricalRecords
try:
dynamic_objects = DynamicField.objects.filter()
# Ensure this logic only runs when the server is started (and only once)
if any(cmd in sys.argv for cmd in ["runserver", "shell"]):
fields_to_remove = DynamicField.objects.filter(remove_column=True)
for df in fields_to_remove:
try:
call_command("delete_field", *(df.pk,))
except Exception as e:
logger.error(e)
for df in dynamic_objects:
field = df.get_field()
field.set_attributes_from_name(df.field_name)
model = df.get_model()
if not column_exists(model._meta.db_table, df.field_name):
logger.info("Field does not exist, adding it.")
with connection.schema_editor() as editor:
editor.add_field(model, field)
model.add_to_class(field.name, field)
name = HistoricalRecords().get_history_model_name(model).lower()
historical_model_ct = ContentType.objects.filter(model=name).first()
if historical_model_ct:
history_model = historical_model_ct.model_class()
if not hasattr(history_model, field.column):
history_model.add_to_class(field.column, field)
except Exception as e:
logger.error(e)
logger.info("ignore if it is fresh installation")
from base.urls import urlpatterns
from django.urls import path, include
urlpatterns.append(
path("df/", include("dynamic_fields.urls")),
)
return super().ready()

View File

@@ -0,0 +1,5 @@
"""
dynamic_fields/df_not_allowed_models.py
"""
DF_NOT_ALLOWED_MODELS = []

161
dynamic_fields/forms.py Normal file
View File

@@ -0,0 +1,161 @@
"""
dynamic_fields/forms.py
"""
from django import forms
from django.utils.translation import gettext_lazy as _
from base.forms import ModelForm
from horilla.horilla_middlewares import _thread_locals
from dynamic_fields import models
from dynamic_fields.df_not_allowed_models import DF_NOT_ALLOWED_MODELS
from dynamic_fields.models import DynamicField
class DynamicFieldForm(ModelForm):
"""
DynamicFieldForm
"""
display_title = _("Add Field")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.pk:
self.fields = {
"verbose_name": self.fields["verbose_name"],
"is_required": self.fields["is_required"],
}
class Meta:
"""
Meta class for additional options
"""
model = models.DynamicField
fields = "__all__"
exclude = [
"model",
"remove_column",
"choices",
]
class ChoiceForm(ModelForm):
"""
ChoiceForm
"""
class Meta:
"""
Meta class for additional option
"""
model = models.Choice
fields = "__all__"
og_init = forms.ModelForm.__init__
og_get_item = forms.ModelForm.__getitem__
class AddFieldWidget(forms.Widget):
"""
Widget to add DynamicFields
"""
template_name = "dynamic_fields/add_df.html"
def __init__(self, attrs=None, form=None):
self.form = form
super().__init__(attrs)
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
context["form"] = self.form
return context
class DFWidget(forms.Widget):
"""
DFWidget
"""
template_name = "dynamic_fields/df.html"
def __init__(self, attrs=None, form=None):
self.form = form
super().__init__(attrs)
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
context["form"] = self.form
return context
def get_item_override(self: forms.ModelForm, name):
"""Return a custom BoundField."""
if name not in self.removed_hdf:
result = og_get_item(self, name)
return result
def init_override(self: forms.ModelForm, *args, **kwargs):
"""
Method to override the ModelForm actual __init__ method
"""
model: models.Model = self._meta.model
model_path = f"{model.__module__}.{model.__name__}"
removed_fields = DynamicField.objects.filter(
model=model_path, remove_column=True
).values_list("field_name", flat=True)
self.removed_hdf = removed_fields
og_init(self, *args, **kwargs)
for df in removed_fields:
if df in self.fields.keys():
del self.fields[df]
other_df = DynamicField.objects.filter(model=model_path, remove_column=False)
for df in other_df:
if df not in self.fields:
form_field = df.get_field().formfield()
form_field.widget = DFWidget(attrs=form_field.widget.attrs, form=self)
attrs = form_field.widget.attrs
attrs["pk"] = df.pk
attrs["class"] = attrs.get("class", "") + "oh-input w-100"
if df.type == "2":
attrs["type"] = "number"
elif df.type == "3":
attrs["type"] = "text_area"
attrs["cols"] = "40"
attrs["rows"] = "2"
elif df.type == "4":
attrs["type"] = "date"
elif df.type == "5":
attrs["type"] = "file"
self.fields[df.field_name] = form_field
if self._meta.fields is not None:
self._meta.fields.append(df.field_name)
request = getattr(_thread_locals, "request")
if (
# self._meta.model in DF_ALLOWED_MODELS and
self._meta.model not in DF_NOT_ALLOWED_MODELS
and request.user.has_perm("dynamic_fields.add_dynamicfield")
):
self.df_user_has_change_perm = request.user.has_perm(
"dynamic_fields.change_dynamicfield"
)
self.df_user_has_delete_perm = request.user.has_perm(
"dynamic_fields.delete_dynamicfield"
)
self.fields["add_df"] = forms.CharField(
label="Add field",
widget=AddFieldWidget(
form=self,
),
required=False,
)
self.df_form_model_path = model_path
forms.ModelForm.__init__ = init_override
forms.ModelForm.__getitem__ = get_item_override

View File

View File

@@ -0,0 +1,66 @@
import logging
from django.db import connection
from django.core.management.base import BaseCommand
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.contrib.contenttypes.models import ContentType
from simple_history.models import HistoricalRecords
from dynamic_fields.models import DynamicField
from dynamic_fields.methods import column_exists
logger = logging.getLogger(__name__)
class Command(BaseCommand):
"""
Command
"""
help = "Save all instances of the specified model"
def add_arguments(self, parser):
parser.add_argument(
"pk",
type=str,
help="Primary key of the DynamicField model to save instances for (e.g., employee.models.Model)",
)
parser.add_argument(
"--is_alter_field",
type=str,
help="Indicates if the field is being altered",
default=None, # optional argument
)
def handle(self, *args, **kwargs):
pk = kwargs["pk"]
editor = BaseDatabaseSchemaEditor(connection)
instance = DynamicField.objects.get(pk=pk)
model = instance.get_model()
field = instance.get_field()
field.set_attributes_from_name(instance.field_name)
if not column_exists(model._meta.db_table, instance.field_name):
sql = editor.sql_create_column % {
"table": editor.quote_name(model._meta.db_table),
"column": editor.quote_name(field.column),
"definition": None,
}
editor.execute(sql)
logger.info(
f"Field does not exist, adding it in {model.__class__.__name__}."
)
model.add_to_class(field.column, field)
name = HistoricalRecords().get_history_model_name(model).lower()
historical_model_ct = ContentType.objects.filter(model=name).first()
if historical_model_ct:
history_model = historical_model_ct.model_class()
if not column_exists(history_model._meta.db_table, instance.field_name):
sql = editor.sql_create_column % {
"table": editor.quote_name(history_model._meta.db_table),
"column": editor.quote_name(field.column),
"definition": None,
}
editor.execute(sql)
logger.info(
f"Field does not exist, adding it in {history_model.__class__.__name__}."
)
history_model.add_to_class(field.column, field)

View File

@@ -0,0 +1,59 @@
import logging
from django.db import models
from django.db import connection
from django.core.management.base import BaseCommand
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.contrib.contenttypes.models import ContentType
from simple_history.models import HistoricalRecords
from dynamic_fields.methods import column_exists
from dynamic_fields.models import DynamicField
logger = logging.getLogger(__name__)
class Command(BaseCommand):
"""
Command
"""
help = "Save all instances of the specified model"
def add_arguments(self, parser):
parser.add_argument(
"pk",
type=str,
help="Primary key of the DynamicField model to \
save instances for (e.g., employee.models.Model)",
)
def handle(self, *args, **kwargs):
pk = kwargs["pk"]
editor = BaseDatabaseSchemaEditor(connection)
instance = DynamicField.objects.get(pk=pk)
model = instance.get_model()
field = instance.get_field()
field.set_attributes_from_name(instance.field_name)
sql_delete_column = editor.sql_delete_column.replace("CASCADE", "")
if column_exists(model._meta.db_table, instance.field_name):
sql = sql_delete_column % {
"table": editor.quote_name(model._meta.db_table),
"column": editor.quote_name(field.column),
}
logger.info(f"Field exist, deleting it from {model.__class__.__name__}.")
editor.execute(sql)
models.Model.delete(instance, *(), **{})
name = HistoricalRecords().get_history_model_name(model).lower()
historical_model_ct = ContentType.objects.filter(model=name).first()
if historical_model_ct:
history_model = historical_model_ct.model_class()
if column_exists(history_model._meta.db_table, instance.field_name):
sql = editor.sql_delete_column % {
"table": editor.quote_name(history_model._meta.db_table),
"column": editor.quote_name(field.column),
}
logger.info(
f"Field exist, deleting it from {history_model.__class__.__name__}."
)

35
dynamic_fields/methods.py Normal file
View File

@@ -0,0 +1,35 @@
"""
dynamic_fields/methods.py
"""
from django.db import connection
from django.template.loader import render_to_string
from horilla.horilla_middlewares import _thread_locals
def column_exists(table_name, column_name):
"""
Check if the column exists in the database table.
"""
with connection.cursor() as cursor:
columns = [
col[0]
for col in connection.introspection.get_table_description(
cursor, table_name
)
]
return column_name in columns
def structured(self):
"""
Render the form fields as HTML table rows with Bootstrap styling.
"""
request = getattr(_thread_locals, "request", None)
context = {
"form": self,
"request": request,
}
table_html = render_to_string("dynamic_fields/common/form.html", context)
return table_html

View File

@@ -0,0 +1 @@
from dynamic_fields import signals

180
dynamic_fields/models.py Normal file
View File

@@ -0,0 +1,180 @@
import logging, re
from django import forms
from django.utils.translation import gettext_lazy as _
from django.db import models
from django.utils import timezone
from django.core.management import call_command
from dynamic_fields.df_not_allowed_models import DF_NOT_ALLOWED_MODELS
from horilla.horilla_middlewares import _thread_locals
from horilla_automations.methods.methods import get_model_class
logger = logging.getLogger(__name__)
# Create your models here.
FIELD_MAP = {
"1": models.CharField,
"2": models.IntegerField,
"3": models.TextField,
"4": models.DateField,
"5": models.FileField,
}
# Define the additional arguments for specific fields
ARGS = {
"1": {"max_length": 30, "default": None},
"2": {"default": 0},
"3": {"default": None},
"4": {"default": timezone.now},
"5": {"null": True, "upload_to": "media/dynamic_fields"},
}
TYPE = (
("1", "Character field"),
("2", "Integer field"),
("3", "Text field"),
("4", "Date field"),
("5", "File field"),
)
class Choice(models.Model):
"""
Choice
"""
title = models.CharField(max_length=25)
def __str__(self):
return self.title
class DynamicField(models.Model):
"""
DynamicFields
"""
model = models.CharField(max_length=100)
verbose_name = models.CharField(max_length=30)
field_name = models.CharField(max_length=30, editable=False)
type = models.CharField(max_length=50, choices=TYPE)
choices = models.ManyToManyField(Choice, blank=True)
is_required = models.BooleanField(default=False)
remove_column = models.BooleanField(default=False)
class Meta:
"""
Meta class to additional options
"""
unique_together = ("model", "field_name")
def delete(self, *args, **kwargs):
self.remove_column = True
self.save()
return
def __str__(self):
return f"{self.field_name} | {self.model}"
def get_field(self):
"""
Field generate method
"""
def _args(key):
args = ARGS.get(key, {})
args["blank"] = not self.is_required
args["verbose_name"] = self.verbose_name
args["null"] = True
return args
field_object: models.CharField = {
key: FIELD_MAP[key](**_args(key)) for key in FIELD_MAP
}[self.type]
if self.choices.exists() and self.type == "1":
choices = [(choice.pk, choice.title) for choice in self.choices.all()]
field_object.choices = choices
field_object.remove_column = self.remove_column
return field_object
def get_model(self):
"""
method to get the model
"""
return get_model_class(self.model)
def clean(self):
"""
Clean method to write the validations
"""
if not re.match(r"^[a-zA-Z]+( [a-zA-Z]+)*$", self.verbose_name):
raise forms.ValidationError(
{
"verbose_name": _(
"Name can only contain alphabetic characters,\
and multiple spaces are not allowed."
)
}
)
field_name = "hdf_" + self.verbose_name.lower().replace(" ", "_")
request = getattr(_thread_locals, "request", None)
model = self.model
if not model and request:
model = request.GET.get("df_model_path", "")
if model:
records = DynamicField.objects.filter(model=model).values_list(
"field_name", flat=True
)
if not self.pk and field_name in records:
raise forms.ValidationError(
{"verbose_name": _("Please enter different name")}
)
elif field_name in records.exclude(pk=self.id):
raise forms.ValidationError(
{"verbose_name": _("Please enter different name")}
)
return super().clean()
def save(self, *args, **kwargs):
# instance = self
is_create = self.pk is None
# hdf -> horilla_dynamic_field
field_name = "hdf_" + self.verbose_name.lower().replace(" ", "_")
if is_create:
self.field_name = field_name
super().save(*args, **kwargs)
call_command("add_field", *(self.pk,))
else:
instance = DynamicField.objects.get(pk=self.pk)
model = instance.get_model()
field = instance.get_field()
if self.remove_column:
try:
field_to_remove = instance.field_name
if hasattr(model, field_to_remove):
# Dynamically remove the field
model._meta.local_fields = [
field
for field in model._meta.local_fields
if field.name != field_to_remove
]
setattr(model, instance.field_name, None)
logger.info(f"Field '{field_to_remove}' removed successfully.")
except Exception as e:
logger.info(e)
super().save(*args, **kwargs)
return self
DF_NOT_ALLOWED_MODELS += [
DynamicField,
]

16
dynamic_fields/signals.py Normal file
View File

@@ -0,0 +1,16 @@
"""
dynamic_fields/signals.py
"""
from django.dispatch import receiver
from django.core.management import call_command
from django.db.models.signals import pre_delete
from dynamic_fields.models import DynamicField
@receiver(pre_delete, sender=DynamicField)
def pre_delete_dynamic_field(sender, instance, **kwargs):
"""
method to delete the column from the db before
deleting the dynamic field
"""
call_command("delete_field", *(instance.pk,))

View File

@@ -0,0 +1,38 @@
{% load hdf_tags %}
<fieldset class="module aligned {{ fieldset.classes }}">
{% if fieldset.name %}<h2>{{ fieldset.name }}</h2>{% endif %}
{% if fieldset.description %}
<div class="description">{{ fieldset.description|safe }}</div>
{% endif %}
{% for line in fieldset %}
{% if line|exclude_removed_df %}
<div class="form-row{% if line.fields|length == 1 and line.errors %} errors{% endif %}{% if not line.has_visible_field %} hidden{% endif %}{% for field in line %}{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% endfor %}">
{% if line.fields|length == 1 %}{{ line.errors }}{% else %}<div class="flex-container form-multiline">{% endif %}
{% for field in line %}
<div>
{% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %}
<div class="flex-container{% if not line.fields|length == 1 %} fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% elif field.is_checkbox %} checkbox-row{% endif %}">
{% if field.is_checkbox %}
{{ field.field }}{{ field.label_tag }}
{% else %}
{{ field.label_tag }}
{% if field.is_readonly %}
<div class="readonly">{{ field.contents }}</div>
{% else %}
{{ field.field }}
{% endif %}
{% endif %}
</div>
{% if field.field.help_text %}
<div class="help"{% if field.field.id_for_label %} id="{{ field.field.id_for_label }}_helptext"{% endif %}>
<div>{{ field.field.help_text|safe }}</div>
</div>
{% endif %}
</div>
{% endfor %}
{% if not line.fields|length == 1 %}</div>{% endif %}
</div>
{% endif %}
{% endfor %}
</fieldset>

View File

@@ -0,0 +1,6 @@
<div class="df-container">
<div class="oh-modal" id="dfModal{{ form.df_form_model_path }}" role="dialog" aria-labelledby="dfModal{{ form.df_form_model_path }}" aria-hidden="true">
<div class="oh-modal__dialog" id="dfModalBody{{ form.df_form_model_path }}"></div>
</div>
<span class="oh-checkpoint-badge text-success mb-2" role="button" onclick="$(this).closest('.df-container').find(`[id='dfModal{{ form.df_form_model_path }}']`).addClass('oh-modal--show')" hx-get="{% url 'add-dynamic-field' %}?df_model_path={{form.df_form_model_path}}" hx-target="[id='dfModalBody{{ form.df_form_model_path }}']">Add Field</span>
</div>

View File

@@ -0,0 +1,93 @@
{% load widget_tweaks %} {% load i18n %}
{% load generic_template_filters %}
<style>
.condition-highlight {
background-color: #ffa5000f;
}
</style>
{% if form.verbose_name %}
<div class="oh-modal__dialog-header">
<h2 class="oh-modal__dialog-title" id="createTitle">
{{form.display_title}}
</h2>
<button type="button" class="oh-modal__close--custom" onclick="$(this).closest('.oh-modal--show').removeClass('oh-modal--show')" aria-label="Close" {{form.close_button_attrs|safe}}>
<ion-icon name="close-outline" role="img" class="md hydrated" aria-label="close outline"></ion-icon>
</button>
</div>
{% endif %}
<div class="oh-modal__dialog-body oh-modal__dialog-relative">
{% if form.instance_ids %}
<div class="oh-modal__dialog oh-modal__dialog--navigation m-0 p-0">
<button
hx-get="{{form.previous_url}}?{{form.ids_key}}={{form.instance_id}}&{{request.GET.urlencode}}"
hx-swap="innerHTML"
hx-target="#genericModalBody"
class="oh-modal__diaglog-nav oh-modal__nav-prev"
>
<ion-icon name="chevron-back-outline"></ion-icon>
</button>
<button
hx-get="{{form.next_url}}?{{form.ids_key}}={{form.instance_id}}&{{request.GET.urlencode}}"
hx-swap="innerHTML"
hx-target="#genericModalBody"
class="oh-modal__diaglog-nav oh-modal__nav-next"
>
<ion-icon name="chevron-forward-outline"></ion-icon>
</button>
</div>
{% endif %}
<div class="oh-general__tab-target oh-profile-section" id="{{form.container_id}}">
<div class="oh-profile-section__card row">
<div class="row" style="padding-right: 0;">
<div class="col-12" style="padding-right: 0;">{{ form.non_field_errors }}</div>
{% for field in form.visible_fields %}
<div class="col-12 col-md-{{field|col}}" id="id_{{ field.name }}_parent_div" style="padding-right: 0;">
<div class="oh-label__info" for="id_{{ field.name }}">
<label class="oh-label {% if field.field.required %} required-star{% endif %}" for="id_{{ field.name }}"
>{% trans field.label %}</label
>
{% if field.help_text != '' %}
<span
class="oh-info mr-2"
title="{{ field.help_text|safe }}"
></span>
{% endif %}
</div>
{% if field.field.widget.input_type == 'checkbox' %}
<div class="oh-switch" style="width: 30px">
{{ field|add_class:'oh-switch__checkbox' }}
</div>
{% else %}
<div id="dynamic_field_{{field.name}}">
{{ field|add_class:'form-control' }}
{{ field.errors }}
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% for field in form.hidden_fields %} {{ field }} {% endfor %}
<div class="d-flex flex-row-reverse">
<button
type="submit"
class="oh-btn oh-btn--secondary mt-2 mr-0 pl-4 pr-5 oh-btn--w-100-resp"
{{form.submit_button_attrs|safe}}
>
{% trans 'Save' %}
</button>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function () {
$("select").on("select2:select", function (e) {
$(".leave-message").hide();
$(this).closest("select")[0].dispatchEvent(new Event("change"));
});
});
</script>

View File

@@ -0,0 +1,66 @@
{% load i18n %}
<div class="oh-hover-btn-container" style="width: 100%;">
{% if widget.attrs.type != "text_area" %}
<input type="{% if not widget.attrs.type %}{{ widget.type }}{% else %}{{widget.attrs.type}}{% endif %}" name="{{ widget.name }}"{% if widget.value != None %} value="{{ widget.value }}"{% endif %}{% include "django/forms/widgets/attrs.html" %}>
{% else %}
<textarea name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>{% if widget.value != None %}{{ widget.value }}{% endif %}</textarea>
<script>
$(document).ready(function () {
$("textarea").closest(".col-lg-6").removeClass("col-lg-6").addClass("col-lg-12")
});
</script>
{% endif %}
{% if form.df_user_has_change_perm or form.delete_dynamicfield %}
<div class="oh-hover-btn-drawer">
{% if form.df_user_has_change_perm %}
<button
hx-get="{% url "edit-verbose-name" widget.attrs.pk %}?df_model_path={{form.df_form_model_path}}"
hx-target="[id='dfModalBody{{ form.df_form_model_path }}']"
onclick="$(`[id='dfModal{{ form.df_form_model_path }}']:first`).addClass('oh-modal--show')"
type="button"
class="oh-hover-btn__small"
onclick=""><ion-icon name="create-outline"></ion-icon></button>
{% endif %}
{% if form.df_user_has_delete_perm %}
<button type="button" class="oh-hover-btn__small" onclick="confirmAction(this)"><ion-icon name="trash-outline"></ion-icon></button>
<script>
function confirmAction(element) {
Swal.fire({
title: "{% trans 'Are you sure?' %}",
text: `{% trans "You won't be able to revert this!" %}`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: "{% trans "Proceed" %}"
}).then((result) => {
if (result.isConfirmed) {
$.ajax({
type: "post",
url: '{% url "remove-dynamic-field" %}',
data: {
csrfmiddlewaretoken: getCookie("csrftoken"),
pk: "{{widget.attrs.pk}}",
},
traditional:true,
success: function (response) {
Swal.fire(
"{% trans 'Success' %}",
"{% trans "Column will be permently removed from the table on the next service reload" %}",
'success'
);
setTimeout(() => {
window.location.reload()
}, 1500);
}
});
}
});
}
</script>
{% endif %}
</div>
{% endif %}
</div>

View File

@@ -0,0 +1,81 @@
{% load generic_template_filters %}
<div id="{{view_id}}">
{% for field_tuple in dynamic_create_fields %}
<div
class="oh-modal"
id="dynamicModal{{field_tuple.0}}"
role="dialog"
aria-labelledby="dynamicModal{{field_tuple.0}}"
aria-hidden="true"
>
<div
class="oh-modal__dialog"
id="dynamicModal{{field_tuple.0}}Body"
></div>
</div>
</div>
{% endfor %}
<form id="{{view_id}}Form" hx-post="{{request.path}}?{{request.GET.urlencode}}" hx-encoding="multipart/form-data" hx-swap="outerHTML" {% if hx_confirm %} hx-confirm="{{hx_confirm}}" {% endif %}>{{form.structured}}</form>
{% for field_tuple in dynamic_create_fields %}
<div >
<script class="dynamic_{{field_tuple.0}}_scripts">
{{form.initial|get_item:field_tuple.0}}
$("#{{view_id}}Form [name={{field_tuple.0}}]").val({{form.initial|get_item:field_tuple.0|safe}}).change()
</script>
<form
hidden
id="modalButton{{field_tuple.0}}Form"
hx-get="/dynamic-path-{{field_tuple.0}}-{{request.session.session_key}}?dynamic_field={{field_tuple.0}}"
hx-target="#dynamicModal{{field_tuple.0}}Body"
>
<input type="text" name="dynamic_initial" data-dynamic-field="{{field_tuple.0}}">
<input type="text" name="view_id" value="{{view_id}}">
{% for field in field_tuple.2 %}
<input type="text" name="{{field}}">
{% endfor %}
<button
type="submit"
id="modalButton{{field_tuple.0}}"
onclick="$('#dynamicModal{{field_tuple.0}}').addClass('oh-modal--show');"
>
{{field_tuple.0}}
</button>
</form>
<form hidden id="reload-field{{field_tuple.0}}{{view_id}}" hx-get="{% url "reload-field" %}?form_class_path={{form_class_path}}&dynamic_field={{field_tuple.0}}" hx-target="#dynamic_field_{{field_tuple.0}}">
<input type="text" name="dynamic_initial" data-dynamic-field="{{field_tuple.0}}">
<input type="text" name="view_id" value="{{view_id}}">
<button class="reload-field" data-target="{{field_tuple.0}}">
{{field_tuple.0}}
</button>
</form>
<script class="dynamic_{{field_tuple.0}}_scripts">
$("#{{view_id}}Form [name={{field_tuple.0}}]").change(function (e) {
values = $(this).val();
if (!values) {
values = ""
}
if (values == "dynamic_create") {
$("#modalButton{{field_tuple.0}}").click()
$(this).val("")
}else if (values.includes("dynamic_create")) {
let index = values.indexOf("dynamic_create");
values.splice(index, 1);
$(this).val(values).change();
$("#modalButton{{field_tuple.0}}").parent().find('input[name=dynamic_initial]').val(values)
$("#reload-field{{field_tuple.0}}{{view_id}}").find('input[name=dynamic_initial]').val(values)
$("#modalButton{{field_tuple.0}}").click()
}else if(values) {
$("#modalButton{{field_tuple.0}}").parent().find('input[name=dynamic_initial]').val(values)
$("#reload-field{{field_tuple.0}}{{view_id}}").find('input[name=dynamic_initial]').val(values)
}
});
$("#reload-field{{field_tuple.0}}{{view_id}}").submit(function (e) {
e.preventDefault();
$(this).find("[name=dynamic_initial]").val();
});
</script>
</div>
{% endfor %}
</div>

View File

View File

@@ -0,0 +1,20 @@
"""
dynamic_fields/templatetags/hdf_tags.py
"""
from django import template
register = template.Library()
@register.filter("exclude_removed_df")
def exclude_removed_df(line: object):
"""
Used to exclude the removed dfs from the fieldset
"""
fields = line.fields
rmvdf = line.form.removed_hdf
for field in fields:
if field in rmvdf:
return False
return line

3
dynamic_fields/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

24
dynamic_fields/urls.py Normal file
View File

@@ -0,0 +1,24 @@
"""
dynamic_fields/urls.py
"""
from django.urls import path
from dynamic_fields import views
urlpatterns = [
path(
"add-dynamic-field",
views.DynamicFieldFormView.as_view(),
name="add-dynamic-field",
),
path(
"edit-verbose-name/<int:pk>/",
views.DynamicFieldFormView.as_view(),
name="edit-verbose-name",
),
path(
"remove-dynamic-field",
views.RemoveDf.as_view(),
name="remove-dynamic-field",
),
]

80
dynamic_fields/views.py Normal file
View File

@@ -0,0 +1,80 @@
"""
dynamic_fields/views.py
"""
from django.http import HttpResponse
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.contrib import messages
from django.views.generic import View
from horilla_views.generic.cbv.views import HorillaFormView
from dynamic_fields import models, forms
from dynamic_fields.methods import structured
from horilla.decorators import login_required, permission_required
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("dynamic_fields.change_mailautomation"), name="dispatch"
)
class ChoiceFormView(HorillaFormView):
"""
ChoiceFormView
"""
model = models.DynamicField
form_class = forms.ChoiceForm
is_dynamic_create_view = True
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("dynamic_fields.change_mailautomation"), name="dispatch"
)
class DynamicFieldFormView(HorillaFormView):
"""
DynamicFieldFormView
"""
model = models.DynamicField
form_class = forms.DynamicFieldForm
template_name = "dynamic_fields/form.html"
# dynamic_create_fields = [
# ("choices", ChoiceFormView),
# ]
def __init__(self, **kwargs):
super().__init__(**kwargs)
setattr(self.form_class, "structured", structured)
def form_valid(self, form: forms.DynamicFieldForm) -> HttpResponse:
model_path = self.request.GET["df_model_path"]
if form.is_valid():
if not form.instance.pk:
form.instance.model = model_path
message = _("New field added")
form.save()
messages.success(self.request, message)
return self.HttpResponse("<script>window.location.reload()</script>")
return super().form_valid(form)
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("dynamic_fields.change_mailautomation"), name="dispatch"
)
class RemoveDf(View):
"""
RemoveDf view
"""
def post(self, *args, **kwargs):
"""
Post method
"""
pk = self.request.POST["pk"]
df = models.DynamicField.objects.get(pk=pk)
df.remove_column = True
df.save()
return HttpResponse({"type": "success"})