From d6ab7f5d74e26566ad2583aff39c9e53263d8750 Mon Sep 17 00:00:00 2001 From: Horilla Date: Mon, 6 Jan 2025 16:22:16 +0530 Subject: [PATCH] [ADD] DYNAMIC FIELDS: Add new app - dynamic fields --- dynamic_fields/__init__.py | 0 dynamic_fields/admin.py | 8 + dynamic_fields/apps.py | 62 ++++++ dynamic_fields/df_not_allowed_models.py | 5 + dynamic_fields/forms.py | 161 ++++++++++++++++ dynamic_fields/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/add_field.py | 66 +++++++ .../management/commands/delete_field.py | 59 ++++++ dynamic_fields/methods.py | 35 ++++ dynamic_fields/migrations/__init__.py | 1 + dynamic_fields/models.py | 180 ++++++++++++++++++ dynamic_fields/signals.py | 16 ++ .../templates/admin/includes/fieldset.html | 38 ++++ .../templates/dynamic_fields/add_df.html | 6 + .../templates/dynamic_fields/common/form.html | 93 +++++++++ .../templates/dynamic_fields/df.html | 66 +++++++ .../templates/dynamic_fields/form.html | 81 ++++++++ dynamic_fields/templatetags/__init__.py | 0 dynamic_fields/templatetags/hdf_tags.py | 20 ++ dynamic_fields/tests.py | 3 + dynamic_fields/urls.py | 24 +++ dynamic_fields/views.py | 80 ++++++++ 23 files changed, 1004 insertions(+) create mode 100644 dynamic_fields/__init__.py create mode 100644 dynamic_fields/admin.py create mode 100644 dynamic_fields/apps.py create mode 100644 dynamic_fields/df_not_allowed_models.py create mode 100644 dynamic_fields/forms.py create mode 100644 dynamic_fields/management/__init__.py create mode 100644 dynamic_fields/management/commands/__init__.py create mode 100644 dynamic_fields/management/commands/add_field.py create mode 100644 dynamic_fields/management/commands/delete_field.py create mode 100644 dynamic_fields/methods.py create mode 100644 dynamic_fields/migrations/__init__.py create mode 100644 dynamic_fields/models.py create mode 100644 dynamic_fields/signals.py create mode 100644 dynamic_fields/templates/admin/includes/fieldset.html create mode 100644 dynamic_fields/templates/dynamic_fields/add_df.html create mode 100644 dynamic_fields/templates/dynamic_fields/common/form.html create mode 100644 dynamic_fields/templates/dynamic_fields/df.html create mode 100644 dynamic_fields/templates/dynamic_fields/form.html create mode 100644 dynamic_fields/templatetags/__init__.py create mode 100644 dynamic_fields/templatetags/hdf_tags.py create mode 100644 dynamic_fields/tests.py create mode 100644 dynamic_fields/urls.py create mode 100644 dynamic_fields/views.py diff --git a/dynamic_fields/__init__.py b/dynamic_fields/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dynamic_fields/admin.py b/dynamic_fields/admin.py new file mode 100644 index 000000000..1d4aab74d --- /dev/null +++ b/dynamic_fields/admin.py @@ -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) diff --git a/dynamic_fields/apps.py b/dynamic_fields/apps.py new file mode 100644 index 000000000..edc9f4214 --- /dev/null +++ b/dynamic_fields/apps.py @@ -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() diff --git a/dynamic_fields/df_not_allowed_models.py b/dynamic_fields/df_not_allowed_models.py new file mode 100644 index 000000000..ee883317f --- /dev/null +++ b/dynamic_fields/df_not_allowed_models.py @@ -0,0 +1,5 @@ +""" +dynamic_fields/df_not_allowed_models.py +""" + +DF_NOT_ALLOWED_MODELS = [] diff --git a/dynamic_fields/forms.py b/dynamic_fields/forms.py new file mode 100644 index 000000000..4b021b9d2 --- /dev/null +++ b/dynamic_fields/forms.py @@ -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 diff --git a/dynamic_fields/management/__init__.py b/dynamic_fields/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dynamic_fields/management/commands/__init__.py b/dynamic_fields/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dynamic_fields/management/commands/add_field.py b/dynamic_fields/management/commands/add_field.py new file mode 100644 index 000000000..418b9cf68 --- /dev/null +++ b/dynamic_fields/management/commands/add_field.py @@ -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) diff --git a/dynamic_fields/management/commands/delete_field.py b/dynamic_fields/management/commands/delete_field.py new file mode 100644 index 000000000..d53c8bbec --- /dev/null +++ b/dynamic_fields/management/commands/delete_field.py @@ -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__}." + ) diff --git a/dynamic_fields/methods.py b/dynamic_fields/methods.py new file mode 100644 index 000000000..e5a995b82 --- /dev/null +++ b/dynamic_fields/methods.py @@ -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 diff --git a/dynamic_fields/migrations/__init__.py b/dynamic_fields/migrations/__init__.py new file mode 100644 index 000000000..6f5ff921e --- /dev/null +++ b/dynamic_fields/migrations/__init__.py @@ -0,0 +1 @@ +from dynamic_fields import signals diff --git a/dynamic_fields/models.py b/dynamic_fields/models.py new file mode 100644 index 000000000..70f3bf203 --- /dev/null +++ b/dynamic_fields/models.py @@ -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, +] diff --git a/dynamic_fields/signals.py b/dynamic_fields/signals.py new file mode 100644 index 000000000..f21aa8d36 --- /dev/null +++ b/dynamic_fields/signals.py @@ -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,)) diff --git a/dynamic_fields/templates/admin/includes/fieldset.html b/dynamic_fields/templates/admin/includes/fieldset.html new file mode 100644 index 000000000..11e1b7ee3 --- /dev/null +++ b/dynamic_fields/templates/admin/includes/fieldset.html @@ -0,0 +1,38 @@ +{% load hdf_tags %} +
+ {% if fieldset.name %}

{{ fieldset.name }}

{% endif %} + {% if fieldset.description %} +
{{ fieldset.description|safe }}
+ {% endif %} + {% for line in fieldset %} + {% if line|exclude_removed_df %} +
+ {% if line.fields|length == 1 %}{{ line.errors }}{% else %}
{% endif %} + {% for field in line %} +
+ {% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %} +
+ {% if field.is_checkbox %} + {{ field.field }}{{ field.label_tag }} + {% else %} + {{ field.label_tag }} + {% if field.is_readonly %} +
{{ field.contents }}
+ {% else %} + {{ field.field }} + {% endif %} + {% endif %} +
+ {% if field.field.help_text %} +
+
{{ field.field.help_text|safe }}
+
+ {% endif %} +
+ {% endfor %} + {% if not line.fields|length == 1 %}
{% endif %} +
+ {% endif %} + {% endfor %} + +
diff --git a/dynamic_fields/templates/dynamic_fields/add_df.html b/dynamic_fields/templates/dynamic_fields/add_df.html new file mode 100644 index 000000000..8109c76b3 --- /dev/null +++ b/dynamic_fields/templates/dynamic_fields/add_df.html @@ -0,0 +1,6 @@ +
+ + Add Field +
diff --git a/dynamic_fields/templates/dynamic_fields/common/form.html b/dynamic_fields/templates/dynamic_fields/common/form.html new file mode 100644 index 000000000..8ed6efdd9 --- /dev/null +++ b/dynamic_fields/templates/dynamic_fields/common/form.html @@ -0,0 +1,93 @@ +{% load widget_tweaks %} {% load i18n %} +{% load generic_template_filters %} + +{% if form.verbose_name %} +
+

+ {{form.display_title}} +

+ +
+{% endif %} +
+ {% if form.instance_ids %} +
+ + + +
+ {% endif %} +
+
+
+
{{ form.non_field_errors }}
+ {% for field in form.visible_fields %} +
+
+ + {% if field.help_text != '' %} + + {% endif %} +
+ + {% if field.field.widget.input_type == 'checkbox' %} +
+ {{ field|add_class:'oh-switch__checkbox' }} +
+ {% else %} +
+ {{ field|add_class:'form-control' }} + {{ field.errors }} +
+ {% endif %} +
+ {% endfor %} +
+ + {% for field in form.hidden_fields %} {{ field }} {% endfor %} + +
+ +
+
+
+
+ diff --git a/dynamic_fields/templates/dynamic_fields/df.html b/dynamic_fields/templates/dynamic_fields/df.html new file mode 100644 index 000000000..9ba91b1c2 --- /dev/null +++ b/dynamic_fields/templates/dynamic_fields/df.html @@ -0,0 +1,66 @@ +{% load i18n %} +
+ {% if widget.attrs.type != "text_area" %} + + {% else %} + + + + {% endif %} + {% if form.df_user_has_change_perm or form.delete_dynamicfield %} +
+ {% if form.df_user_has_change_perm %} + + {% endif %} + {% if form.df_user_has_delete_perm %} + + + {% endif %} +
+ {% endif %} +
\ No newline at end of file diff --git a/dynamic_fields/templates/dynamic_fields/form.html b/dynamic_fields/templates/dynamic_fields/form.html new file mode 100644 index 000000000..79d3e0e47 --- /dev/null +++ b/dynamic_fields/templates/dynamic_fields/form.html @@ -0,0 +1,81 @@ +{% load generic_template_filters %} +
+ {% for field_tuple in dynamic_create_fields %} + +
+ {% endfor %} +
{{form.structured}}
+ {% for field_tuple in dynamic_create_fields %} +
+ + + + + +
+ {% endfor %} + + diff --git a/dynamic_fields/templatetags/__init__.py b/dynamic_fields/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dynamic_fields/templatetags/hdf_tags.py b/dynamic_fields/templatetags/hdf_tags.py new file mode 100644 index 000000000..3b75365cc --- /dev/null +++ b/dynamic_fields/templatetags/hdf_tags.py @@ -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 diff --git a/dynamic_fields/tests.py b/dynamic_fields/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/dynamic_fields/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/dynamic_fields/urls.py b/dynamic_fields/urls.py new file mode 100644 index 000000000..fe26f7713 --- /dev/null +++ b/dynamic_fields/urls.py @@ -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//", + views.DynamicFieldFormView.as_view(), + name="edit-verbose-name", + ), + path( + "remove-dynamic-field", + views.RemoveDf.as_view(), + name="remove-dynamic-field", + ), +] diff --git a/dynamic_fields/views.py b/dynamic_fields/views.py new file mode 100644 index 000000000..6a7cb7f6d --- /dev/null +++ b/dynamic_fields/views.py @@ -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("") + 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"})