[ADD] DYNAMIC FIELDS: Add new app - dynamic fields
This commit is contained in:
0
dynamic_fields/__init__.py
Normal file
0
dynamic_fields/__init__.py
Normal file
8
dynamic_fields/admin.py
Normal file
8
dynamic_fields/admin.py
Normal 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
62
dynamic_fields/apps.py
Normal 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()
|
||||
5
dynamic_fields/df_not_allowed_models.py
Normal file
5
dynamic_fields/df_not_allowed_models.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
dynamic_fields/df_not_allowed_models.py
|
||||
"""
|
||||
|
||||
DF_NOT_ALLOWED_MODELS = []
|
||||
161
dynamic_fields/forms.py
Normal file
161
dynamic_fields/forms.py
Normal 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
|
||||
0
dynamic_fields/management/__init__.py
Normal file
0
dynamic_fields/management/__init__.py
Normal file
0
dynamic_fields/management/commands/__init__.py
Normal file
0
dynamic_fields/management/commands/__init__.py
Normal file
66
dynamic_fields/management/commands/add_field.py
Normal file
66
dynamic_fields/management/commands/add_field.py
Normal 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)
|
||||
59
dynamic_fields/management/commands/delete_field.py
Normal file
59
dynamic_fields/management/commands/delete_field.py
Normal 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
35
dynamic_fields/methods.py
Normal 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
|
||||
1
dynamic_fields/migrations/__init__.py
Normal file
1
dynamic_fields/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from dynamic_fields import signals
|
||||
180
dynamic_fields/models.py
Normal file
180
dynamic_fields/models.py
Normal 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
16
dynamic_fields/signals.py
Normal 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,))
|
||||
38
dynamic_fields/templates/admin/includes/fieldset.html
Normal file
38
dynamic_fields/templates/admin/includes/fieldset.html
Normal 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>
|
||||
6
dynamic_fields/templates/dynamic_fields/add_df.html
Normal file
6
dynamic_fields/templates/dynamic_fields/add_df.html
Normal 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>
|
||||
93
dynamic_fields/templates/dynamic_fields/common/form.html
Normal file
93
dynamic_fields/templates/dynamic_fields/common/form.html
Normal 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>
|
||||
66
dynamic_fields/templates/dynamic_fields/df.html
Normal file
66
dynamic_fields/templates/dynamic_fields/df.html
Normal 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>
|
||||
81
dynamic_fields/templates/dynamic_fields/form.html
Normal file
81
dynamic_fields/templates/dynamic_fields/form.html
Normal 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>
|
||||
|
||||
0
dynamic_fields/templatetags/__init__.py
Normal file
0
dynamic_fields/templatetags/__init__.py
Normal file
20
dynamic_fields/templatetags/hdf_tags.py
Normal file
20
dynamic_fields/templatetags/hdf_tags.py
Normal 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
3
dynamic_fields/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
24
dynamic_fields/urls.py
Normal file
24
dynamic_fields/urls.py
Normal 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
80
dynamic_fields/views.py
Normal 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"})
|
||||
Reference in New Issue
Block a user