From f9b08d5be5da4dcbcdc94b4d05f700f98632c4f9 Mon Sep 17 00:00:00 2001 From: Fotiadis Alexandros Date: Wed, 27 Oct 2021 13:46:34 +0200 Subject: [PATCH 1/4] feat: support chainfield with filtered selection widget. --- .../smart-selects/admin/js/chainedm2m.js | 58 +++++++++++++++---- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/smart_selects/static/smart-selects/admin/js/chainedm2m.js b/smart_selects/static/smart-selects/admin/js/chainedm2m.js index 99a4cbb..e3d469f 100755 --- a/smart_selects/static/smart-selects/admin/js/chainedm2m.js +++ b/smart_selects/static/smart-selects/admin/js/chainedm2m.js @@ -134,27 +134,63 @@ trigger_chosen_updated(); }); }, - init: function (chainfield, url, id, value, auto_choose) { var fill_field, val, initial_parent = $(chainfield).val(), initial_value = value; + // In case of filtered chainfield. + var chainfield_filtered = chainfield + '_to'; + if (!$(chainfield).hasClass("chained")) { val = $(chainfield).val(); this.fill_field(val, initial_value, id, url, initial_parent, auto_choose); } fill_field = this.fill_field; - $(chainfield).change(function () { - var prefix, start_value, this_val, localID = id; - if (localID.indexOf("__prefix__") > -1) { - prefix = $(this).attr("id").match(/\d+/)[0]; - localID = localID.replace("__prefix__", prefix); - } - start_value = $(localID).val(); - this_val = $(this).val(); - fill_field(this_val, initial_value, localID, url, initial_parent, auto_choose); - }); + if(!$(chainfield).length && $(chainfield_filtered).length) { + /** + * Handle chainfield with filtered elements. + * + * The option selection is completely different compared to the `$(chainfield).change` call. + * + * Keep track on DOM modifications and submit the fill_field function using the + * rendered options of the element `chainfield_filtered` ( #id__to ). + */ + + var options = 0; + var timeoutID = null; + + $(chainfield_filtered).on("DOMSubtreeModified", function(event) { + var optionElements = $(chainfield_filtered).children('option') + if(options !== optionElements.length) { + options = optionElements.length + + var val = optionElements.map((idx, ele) => $(ele).val()) + + /** + * DEBOUNCE, avoid redundant calls + * DOMSubtreeModified is invoked multiple times during the multiple + * selection, avoid redundant queries using the timeout as debounce. + */ + if(timeoutID) clearTimeout(timeoutID) + timeoutID = setTimeout(function() { + fill_field(val.length ? Array.from(val) : false, initial_value, id, url, initial_parent, auto_choose); + }, 1000); + } + }) + } else { + $(chainfield).change(function () { + var prefix, start_value, this_val, localID = id; + if (localID.indexOf("__prefix__") > -1) { + prefix = $(this).attr("id").match(/\d+/)[0]; + localID = localID.replace("__prefix__", prefix); + } + + start_value = $(localID).val(); + this_val = $(this).val(); + fill_field(this_val, initial_value, localID, url, initial_parent, auto_choose); + }); + } // allait en bas, hors du documentready if (typeof(dismissAddAnotherPopup) !== 'undefined') { From e7fa12c047ca000d61c56d3b646b1027025126d3 Mon Sep 17 00:00:00 2001 From: Fotiadis Alexandros Date: Wed, 27 Oct 2021 15:07:14 +0200 Subject: [PATCH 2/4] refactor: remove window load and document ready window-load is dispatched before jquery elements are initiated, can lead to issues when the chainfield is a filtered-horizontal --- .../static/smart-selects/admin/js/bindfields.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/smart_selects/static/smart-selects/admin/js/bindfields.js b/smart_selects/static/smart-selects/admin/js/bindfields.js index 295d563..fe9b9fa 100755 --- a/smart_selects/static/smart-selects/admin/js/bindfields.js +++ b/smart_selects/static/smart-selects/admin/js/bindfields.js @@ -20,7 +20,10 @@ } } - $(window).on('load', function () { + $(document).ready(function () { + $.each($(".chained-fk"), function (index, item) { + initItem(item); + }); $.each($(".chained"), function (index, item) { initItem(item); }); @@ -31,12 +34,6 @@ }); }); - $(document).ready(function () { - $.each($(".chained-fk"), function (index, item) { - initItem(item); - }); - }); - function initFormset(chained) { var re = /\d+/g, prefix, From af69f396999445517a53fb8692c491ff8a9426cb Mon Sep 17 00:00:00 2001 From: Fotiadis Alexandros Date: Wed, 27 Oct 2021 15:08:42 +0200 Subject: [PATCH 3/4] refactor: perform initial fill form --- .../static/smart-selects/admin/js/chainedm2m.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/smart_selects/static/smart-selects/admin/js/chainedm2m.js b/smart_selects/static/smart-selects/admin/js/chainedm2m.js index e3d469f..5948321 100755 --- a/smart_selects/static/smart-selects/admin/js/chainedm2m.js +++ b/smart_selects/static/smart-selects/admin/js/chainedm2m.js @@ -147,7 +147,7 @@ } fill_field = this.fill_field; - if(!$(chainfield).length && $(chainfield_filtered).length) { + if($(chainfield_filtered).length) { /** * Handle chainfield with filtered elements. * @@ -160,7 +160,7 @@ var options = 0; var timeoutID = null; - $(chainfield_filtered).on("DOMSubtreeModified", function(event) { + function onSubtreeModified() { var optionElements = $(chainfield_filtered).children('option') if(options !== optionElements.length) { options = optionElements.length @@ -175,9 +175,14 @@ if(timeoutID) clearTimeout(timeoutID) timeoutID = setTimeout(function() { fill_field(val.length ? Array.from(val) : false, initial_value, id, url, initial_parent, auto_choose); - }, 1000); + }, 200); } - }) + } + + $(chainfield_filtered).on("DOMSubtreeModified", onSubtreeModified) + + // initial + onSubtreeModified() } else { $(chainfield).change(function () { var prefix, start_value, this_val, localID = id; From 6abab46b297a83fecf1504b49bcda65468e09ed4 Mon Sep 17 00:00:00 2001 From: Fotiadis Alexandros Date: Wed, 27 Oct 2021 15:09:11 +0200 Subject: [PATCH 4/4] test: add book-store example with manytomany filtered chainfield --- test_app/admin.py | 7 +- .../migrations/0008_auto_20211027_1307.py | 140 ++++++++++++++++++ test_app/models.py | 26 ++++ 3 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 test_app/migrations/0008_auto_20211027_1307.py diff --git a/test_app/admin.py b/test_app/admin.py index 7674601..6f33a37 100644 --- a/test_app/admin.py +++ b/test_app/admin.py @@ -6,6 +6,7 @@ Location, Publication, Book, + BookStore, Writer, Grade, Team, @@ -47,7 +48,6 @@ class AreaAdmin(admin.ModelAdmin): class LocationAdmin(admin.ModelAdmin): list_display = ("continent", "country", "city", "street") - class PublicationAdmin(admin.ModelAdmin): list_display = ("name",) @@ -56,6 +56,10 @@ class BookAdmin(admin.ModelAdmin): list_display = ("name",) +class BookStoreAdmin(admin.ModelAdmin): + list_display = ('name', ) + filter_horizontal = ('books', ) + class WriterAdmin(admin.ModelAdmin): list_display = ("name",) @@ -127,3 +131,4 @@ class TalentAdmin(admin.ModelAdmin): admin.site.register(Person, PersonAdmin) admin.site.register(Group, GroupAdmin) admin.site.register(Talent, TalentAdmin) +admin.site.register(BookStore, BookStoreAdmin) \ No newline at end of file diff --git a/test_app/migrations/0008_auto_20211027_1307.py b/test_app/migrations/0008_auto_20211027_1307.py new file mode 100644 index 0000000..b7fe0ab --- /dev/null +++ b/test_app/migrations/0008_auto_20211027_1307.py @@ -0,0 +1,140 @@ +# Generated by Django 3.2.8 on 2021-10-27 13:07 + +from django.db import migrations, models +import django.db.models.deletion +import smart_selects.db_fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('test_app', '0007_auto_20200207_1149'), + ] + + operations = [ + migrations.AlterField( + model_name='area', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='book', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='book1', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='book1', + name='writer', + field=smart_selects.db_fields.ChainedManyToManyField(chained_field='publication', chained_model_field='publications', limit_choices_to={'name__contains': '2'}, to='test_app.Writer'), + ), + migrations.AlterField( + model_name='client', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='continent', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='country', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='domain', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='grade', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='group', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='location', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='location1', + name='country', + field=smart_selects.db_fields.ChainedForeignKey(auto_choose=True, chained_field='continent', chained_model_field='continent', limit_choices_to={'name__startswith': 'G'}, on_delete=django.db.models.deletion.CASCADE, to='test_app.country'), + ), + migrations.AlterField( + model_name='location1', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='membership', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='person', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='publication', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='student', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='tag', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='tagresource', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='talent', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='team', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='website', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='writer', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.CreateModel( + name='BookStore', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('books', models.ManyToManyField(blank=True, to='test_app.Book')), + ('publications', smart_selects.db_fields.ChainedManyToManyField(chained_field='books', chained_model_field='book', horizontal=True, related_name='stores', to='test_app.Publication')), + ('writers', smart_selects.db_fields.ChainedManyToManyField(chained_field='books', chained_model_field='book', related_name='stores', to='test_app.Writer')), + ], + ), + ] diff --git a/test_app/models.py b/test_app/models.py index e68cd8f..a56d7c4 100644 --- a/test_app/models.py +++ b/test_app/models.py @@ -90,6 +90,9 @@ class Book(models.Model): ) name = models.CharField(max_length=255) + def __str__(self): + return "%s" % self.name + # test limit_to_choice field option class Book1(models.Model): @@ -102,6 +105,29 @@ class Book1(models.Model): ) name = models.CharField(max_length=255) +# test based on chainfield with filtered many_to_many + +class BookStore(models.Model): + name = models.CharField(max_length=255) + books = models.ManyToManyField('Book', blank=True) + + publications = ChainedManyToManyField( + "Publication", + chained_field="books", + chained_model_field="book", + related_name="stores", + horizontal=True + ) + + writers = ChainedManyToManyField( + "Writer", + chained_field="books", + chained_model_field="book", + related_name="stores" + ) + + + # group foreignkey class Grade(models.Model):