4

I recently upgraded to Django 2.2.2 and Python 3.6.8, and my filter_horizontal feature in Django admin has disappeared.

I tried viewing my admin in Chrome incognito mode, as some answers suggest, and I also tried changing verbose_name strings to unicode. However, none of these worked.

Here is an example of a model for which I am attempting to show filter_horizontal. This worked on my app prior to the upgrades.

admin.py

class ResearchAdmin(admin.ModelAdmin):
    filter_horizontal = ('criteria', 'state')
    
    def save_model(self, request, obj, form, change):
        obj.save()

        # Update cache for tasks containing the saved goal
        tasks = Task.objects.filter(research__id=obj.pk).values_list('pk', flat=True)
        for t in tasks:
            cache.delete_many(['task%s' % t, 'task%s_research' % t])

models.py

    """
        Clinical Research model
    """
    def __str__(self):
        return "%s" % (self.name)

    name = models.CharField(max_length=50, null=True, blank=True)
    type = models.CharField(max_length=50, null=True, blank=True)
    cta = models.CharField(max_length=50, null=True, blank=True)
    description = models.TextField(null=True, blank=True)
    picture = models.ImageField(upload_to='images/%Y/%m/%d', null=True, blank=True, help_text="Upload portrait image for modal study description")
    layout = models.CharField(max_length=1, choices=LAYOUT_TYPE, null=False, blank=False, default='r')
    criteria = models.ManyToManyField('Answer', blank=True, db_index=True, help_text="Answers required for qualified patients")
    required_contact = models.ManyToManyField('ContactField', blank=True, db_index=True, help_text="Contact info for patient to enter")
    email = models.EmailField(null=True, blank=True, help_text="Sponsor email for notifying of screened patients")
    link = models.URLField(null=True, blank=True)
    state = models.ManyToManyField('State', blank=True, help_text="Qualifying states")
    lat = models.CharField(max_length=60, null=True, blank=True)
    lng = models.CharField(max_length=60, null=True, blank=True)
    distance = models.PositiveIntegerField(null=True, blank=True, help_text="Maximum distance from user in miles to show")

    class Meta:
        verbose_name = u"Research"
        verbose_name_plural = u"Research"

There are no error messages, but the filter_horizontal frontend doesn't show up in admin for the criteria and state fields.

##collectstatic## As @iain-shelvington suggested, this issue may be due to some kind of interference with the cached front-end code required to display the filter_horizontal format. I have tried running in Google Incognito mode, clearing catch, and running collectstatic --clear, and none of these work. Moreover, there aren't any differences between the admin static files pre- and post- upgrade.

@SylvainBiehler pointed out that django_gulp may be overriding collectstatic. I disabled django_gulp and ran ./manage.py collectstatic --clear all admin files are now updated post Django upgrade.

##Comparing admin files pre- and post- Django upgrade## I was able to spin up a version of my app pre-Django upgrade, and the filter_horizontal capability works in the older version. There are some differences in the construction of the Criteria field from Chrome console:

Old Version (works)

Select element prior to choices:

<select multiple="multiple" class="selectfilter" id="id_criteria" name="criteria">

Javascript after choices:

<script type="text/javascript">addEvent(window, "load", function(e) {SelectFilter.init("id_criteria", "criteria", 0, "https://xxxxxxx/static/admin/"); });</script>

New Version (broken)

Slightly different select element. No javascript after choices:

<select name="criteria" id="id_criteria" multiple class="selectfilter" data-field-name="criteria" data-is-stacked="0">

This seems to be causing the issue, but I have no idea how to fix it. Any ideas?

##Analyzing admin JS in console## SelectBox.js loads in the console and declares var = SelectBox = {... which is never called.

SelectFilter.js also loads but the function is never called:

/*
SelectFilter2 - Turns a multiple-select box into a filter interface.

Requires core.js, SelectBox.js and addevent.js.
*/
(function($) {
function findForm(node) {
    // returns the node of the form containing the given node
    if (node.tagName.toLowerCase() != 'form') {
        return findForm(node.parentNode);
    }
    return node;
}

window.SelectFilter = {
    init: function(field_id, field_name, is_stacked, admin_static_prefix) {
        if (field_id.match(/__prefix__/)){
            // Don't intialize on empty forms.
            return;
        }
        var from_box = document.getElementById(field_id);
        from_box.id += '_from'; // change its ID
        from_box.className = 'filtered';

        var ps = from_box.parentNode.getElementsByTagName('p');
        for (var i=0; i<ps.length; i++) {
            if (ps[i].className.indexOf("info") != -1) {
                // Remove <p class="info">, because it just gets in the way.
                from_box.parentNode.removeChild(ps[i]);
            } else if (ps[i].className.indexOf("help") != -1) {
                // Move help text up to the top so it isn't below the select
                // boxes or wrapped off on the side to the right of the add
                // button:
                from_box.parentNode.insertBefore(ps[i], from_box.parentNode.firstChild);
            }
        }

        // <div class="selector"> or <div class="selector stacked">
        var selector_div = quickElement('div', from_box.parentNode);
        selector_div.className = is_stacked ? 'selector stacked' : 'selector';

        // <div class="selector-available">
        var selector_available = quickElement('div', selector_div, '');
        selector_available.className = 'selector-available';
        var title_available = quickElement('h2', selector_available, interpolate(gettext('Available %s') + ' ', [field_name]));
        quickElement('img', title_available, '', 'src', admin_static_prefix + 'img/icon-unknown.gif', 'width', '10', 'height', '10', 'class', 'help help-tooltip', 'title', interpolate(gettext('This is the list of available %s. You may choose some by selecting them in the box below and then clicking the "Choose" arrow between the two boxes.'), [field_name]));

        var filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter');
        filter_p.className = 'selector-filter';

        var search_filter_label = quickElement('label', filter_p, '', 'for', field_id + "_input");

        var search_selector_img = quickElement('img', search_filter_label, '', 'src', admin_static_prefix + 'img/selector-search.gif', 'class', 'help-tooltip', 'alt', '', 'title', interpolate(gettext("Type into this box to filter down the list of available %s."), [field_name]));

        filter_p.appendChild(document.createTextNode(' '));

        var filter_input = quickElement('input', filter_p, '', 'type', 'text', 'placeholder', gettext("Filter"));
        filter_input.id = field_id + '_input';

        selector_available.appendChild(from_box);
        var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', 'javascript: (function(){ SelectBox.move_all("' + field_id + '_from", "' + field_id + '_to"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_add_all_link');
        choose_all.className = 'selector-chooseall';

        // <ul class="selector-chooser">
        var selector_chooser = quickElement('ul', selector_div, '');
        selector_chooser.className = 'selector-chooser';
        var add_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Choose'), 'title', gettext('Choose'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_from","' + field_id + '_to"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_add_link');
        add_link.className = 'selector-add';
        var remove_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Remove'), 'title', gettext('Remove'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_to","' + field_id + '_from"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_remove_link');
        remove_link.className = 'selector-remove';

        // <div class="selector-chosen">
        var selector_chosen = quickElement('div', selector_div, '');
        selector_chosen.className = 'selector-chosen';
        var title_chosen = quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s') + ' ', [field_name]));
        quickElement('img', title_chosen, '', 'src', admin_static_prefix + 'img/icon-unknown.gif', 'width', '10', 'height', '10', 'class', 'help help-tooltip', 'title', interpolate(gettext('This is the list of chosen %s. You may remove some by selecting them in the box below and then clicking the "Remove" arrow between the two boxes.'), [field_name]));

        var to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', 'multiple', 'size', from_box.size, 'name', from_box.getAttribute('name'));
        to_box.className = 'filtered';
        var clear_all = quickElement('a', selector_chosen, gettext('Remove all'), 'title', interpolate(gettext('Click to remove all chosen %s at once.'), [field_name]), 'href', 'javascript: (function() { SelectBox.move_all("' + field_id + '_to", "' + field_id + '_from"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_remove_all_link');
        clear_all.className = 'selector-clearall';

        from_box.setAttribute('name', from_box.getAttribute('name') + '_old');

        // Set up the JavaScript event handlers for the select box filter interface
        addEvent(filter_input, 'keyup', function(e) { SelectFilter.filter_key_up(e, field_id); });
        addEvent(filter_input, 'keydown', function(e) { SelectFilter.filter_key_down(e, field_id); });
        addEvent(from_box, 'change', function(e) { SelectFilter.refresh_icons(field_id) });
        addEvent(to_box, 'change', function(e) { SelectFilter.refresh_icons(field_id) });
        addEvent(from_box, 'dblclick', function() { SelectBox.move(field_id + '_from', field_id + '_to'); SelectFilter.refresh_icons(field_id); });
        addEvent(to_box, 'dblclick', function() { SelectBox.move(field_id + '_to', field_id + '_from'); SelectFilter.refresh_icons(field_id); });
        addEvent(findForm(from_box), 'submit', function() { SelectBox.select_all(field_id + '_to'); });
        SelectBox.init(field_id + '_from');
        SelectBox.init(field_id + '_to');
        // Move selected from_box options to to_box
        SelectBox.move(field_id + '_from', field_id + '_to');

        if (!is_stacked) {
            // In horizontal mode, give the same height to the two boxes.
            var j_from_box = $(from_box);
            var j_to_box = $(to_box);
            var resize_filters = function() { j_to_box.height($(filter_p).outerHeight() + j_from_box.outerHeight()); }
            if (j_from_box.outerHeight() > 0) {
                resize_filters(); // This fieldset is already open. Resize now.
            } else {
                // This fieldset is probably collapsed. Wait for its 'show' event.
                j_to_box.closest('fieldset').one('show.fieldset', resize_filters);
            }
        }

        // Initial icon refresh
        SelectFilter.refresh_icons(field_id);
    },
    refresh_icons: function(field_id) {
        var from = $('#' + field_id + '_from');
        var to = $('#' + field_id + '_to');
        var is_from_selected = from.find('option:selected').length > 0;
        var is_to_selected = to.find('option:selected').length > 0;
        // Active if at least one item is selected
        $('#' + field_id + '_add_link').toggleClass('active', is_from_selected);
        $('#' + field_id + '_remove_link').toggleClass('active', is_to_selected);
        // Active if the corresponding box isn't empty
        $('#' + field_id + '_add_all_link').toggleClass('active', from.find('option').length > 0);
        $('#' + field_id + '_remove_all_link').toggleClass('active', to.find('option').length > 0);
    },
    filter_key_up: function(event, field_id) {
        var from = document.getElementById(field_id + '_from');
        // don't submit form if user pressed Enter
        if ((event.which && event.which == 13) || (event.keyCode && event.keyCode == 13)) {
            from.selectedIndex = 0;
            SelectBox.move(field_id + '_from', field_id + '_to');
            from.selectedIndex = 0;
            return false;
        }
        var temp = from.selectedIndex;
        SelectBox.filter(field_id + '_from', document.getElementById(field_id + '_input').value);
        from.selectedIndex = temp;
        return true;
    },
    filter_key_down: function(event, field_id) {
        var from = document.getElementById(field_id + '_from');
        // right arrow -- move across
        if ((event.which && event.which == 39) || (event.keyCode && event.keyCode == 39)) {
            var old_index = from.selectedIndex;
            SelectBox.move(field_id + '_from', field_id + '_to');
            from.selectedIndex = (old_index == from.length) ? from.length - 1 : old_index;
            return false;
        }
        // down arrow -- wrap around
        if ((event.which && event.which == 40) || (event.keyCode && event.keyCode == 40)) {
            from.selectedIndex = (from.length == from.selectedIndex + 1) ? 0 : from.selectedIndex + 1;
        }
        // up arrow -- wrap around
        if ((event.which && event.which == 38) || (event.keyCode && event.keyCode == 38)) {
            from.selectedIndex = (from.selectedIndex == 0) ? from.length - 1 : from.selectedIndex - 1;
        }
        return true;
    }
}

})(django.jQuery);

##INSTALLED APPS##

########## APP CONFIGURATION

INSTALLED_APPS = DJANGO_APPS + LOCAL_APPS

DJANGO_APPS = (
    'django_gulp',
    # Default Django apps:
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # Useful template tags:
    # 'django.contrib.humanize',

    # Python-Social-Auth
    'social_django',

    # Admin panel and documentation:
    'django.contrib.admin',
    'django.core.management',
    # 'django.contrib.admindocs',

    # for task queue

    # For django-storages static store on AWS S3
    'storages',

    # Other django apps
    'rest_framework',
)

# Apps specific for this project go here.
LOCAL_APPS = (
    'base',
    'myproject',
    'users',
    'campaigns',
)
wraasch
  • 405
  • 3
  • 13
  • 1
    Have you tried running collectstatic and clearing your browser cache? – Iain Shelvington Jul 26 '19 at 20:21
  • I have run collectstatic several times, including `python3 ./manage.py collectstatic --clear`. I tried using Chrome incognito, cleared the browser cache for my site domain, and checked on somebody else's browser, which had never been on the site. Would that thoroughly rule out the potential cache cause? – wraasch Jul 26 '19 at 22:37
  • can you also share settings.py and check in network console of chrome if it's unable to load any dependencies – Krishna Chaitanya Kornepati Aug 01 '19 at 03:32
  • Sure thing. Which parts of settings.py are most relevant for this? The file itself is >400 lines. There are no errors printed in the Chrome console. – wraasch Aug 01 '19 at 04:27
  • 3
    Can you verify in the console that `SelectBox.js` and `SelectFilter2.js` are loaded? Those two files display the horizontal widget – Sylvain Biehler Aug 01 '19 at 08:11
  • @SylvainBiehler I have updated the question to describe how those files are observed in console. – wraasch Aug 01 '19 at 14:57
  • 1
    Funny because the end of `SelectFilter2.js` contains a trigger that should be called when your select of class selectfilter is loaded – Sylvain Biehler Aug 01 '19 at 15:41
  • @SylvainBiehler I have included the SelectFilter2.js, which is the same as my working version (Django 1.8, Python 2.7). The only difference between the versions is in the admin HTML compile (highlighted in **UPDATE 2**) – wraasch Aug 01 '19 at 16:01
  • can you show the INSTALLED_APPS part of `settings.py`. i think script is running before dom for that element is initialized or an app is missing or misordered in settings.py – Krishna Chaitanya Kornepati Aug 01 '19 at 16:11
  • @KrishnaChaitanyaKornepati I have added my INSTALLED_APPS – wraasch Aug 01 '19 at 17:03
  • I cant find anything out of the ordinary. Can you try `LOCAL_APPS + DJANGO_APPS` instead of `DJANGO_APPS + LOCAL_APPS`. sometimes the order matters. – Krishna Chaitanya Kornepati Aug 01 '19 at 22:04
  • @KrishnaChaitanyaKornepati Unfortunately, that didn't work. I have a feeling it has something to do with the way that the admin compiles (see **UPDATE 2**) – wraasch Aug 01 '19 at 22:36
  • I would start from checking your chrome development console output. If there are some errors of warnings - you should take a look on them. There can be a lot of different issues, like issues with loading jquery or some error in your own scripts – Alexandr Zayets Aug 02 '19 at 08:17
  • 2
    since `django_gulp` overrides `collectstatic` and `runserver`, can you retry `collectstatic --clear` without `django_gulp` ? – Sylvain Biehler Aug 02 '19 at 09:58
  • 1
    @SylvainBiehler I think you are right that the issue has something to do with `collectstatic` because my admin static files (stored on S3) were last modified months ago despite running `collectstatic --clear` (with & without `django_gulp`) today. When I run `collectstatic --clear --verbosity 2`, it shows that it is skipping, rather than deleting unmodified files. Still looking into how to fix this. – wraasch Aug 02 '19 at 14:41
  • 1
    @wraasch Can you move current staticfiles to somewhere temporarily and then run collectstatic again? – Chillar Anand Aug 07 '19 at 13:45
  • @ChillarAnand That worked for updating the files, but didn't solve the `filter_horizontal` issue. – wraasch Aug 07 '19 at 15:34

1 Answers1

0

I copied the static/admin files from Django.contrib using the following command in terminal:

cp -a /Users/username/.virtualenvs/rs/lib/python3.9/site-packages/django/contrib/admin/. /Users/username/Documents/myproject/static/

I then ran collectstatic to upload the files to S3, where they are stored for production. This actually worked, but seems a bit hacky. I must not be doing something right for these files to not update upon Django upgrade.

wraasch
  • 405
  • 3
  • 13