Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed #14370 -- Added select2 widget for related object fields in admin. #6385

Merged
merged 1 commit into from Sep 18, 2017

Conversation

codingjoe
Copy link
Sponsor Contributor

@codingjoe codingjoe commented Apr 2, 2016

Adds jQuery Select2 version 4 to support async select inputs
including a search feature.

I split the PR in two commits, one is vendoring select2, one contains my code.

Links & Discussions

Changes:

  • jQuery noConflict is set to false, jQuery itself does not get removed form global
  • the new select2 widget is automatically used if the related object
    has a registered admin and defines search fields
  • only str representation is supported at this point
  • the search feature uses the same field as the model admin

Todo:

  • Possible deprecation of raw_id field?
  • Release note. (Which release?)
  • Selenium integration tests
  • widget tests
  • pagingnator and page tests
  • view tests
  • admin_site integration tests
  • add MODEL_change permission to json view
  • system checks

@codingjoe
Copy link
Sponsor Contributor Author

@jpic this is a first draft, I tested it manually, it seems fine
@timgraham I only added tests for the new json view, I'll add the widget tests in the sprint tomorrow

@codingjoe codingjoe force-pushed the issues/14370 branch 6 times, most recently from bcaa59d to 1ba7b5f Compare April 3, 2016 00:38
@@ -384,3 +389,97 @@ def url_for_result(self, result):
self.opts.model_name),
args=(quote(pk),),
current_app=self.model_admin.admin_site.name)


class ForeignKeyJsonView(BaseListView):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't there be a LoginRequiredMixin to prevent unauthorized access?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my bad, wrap does it (thanks @codingjoe)

@jpic
Copy link
Contributor

jpic commented Apr 3, 2016

Nice !

Did you try it on dynamically added formset rows ?

@codingjoe
Copy link
Sponsor Contributor Author

@jpic no that can't work yet, but I'm on it today!
I also got a lot of documentation ahead of me, but it really depends where this is going. I'm gonna talk to a couple of people here about it.

@jpic
Copy link
Contributor

jpic commented Apr 3, 2016

@codingjoe we use DOMNodeInserted event for dynamically added selects: https://github.com/yourlabs/django-autocomplete-light/blob/master/src/dal/static/autocomplete_light/autocomplete.init.js#L25-L27

We also have a really small snippet for option renaming using the edit button which you might like: https://github.com/yourlabs/django-autocomplete-light/blob/master/src/dal_select2/static/autocomplete_light/select2.js#L100-L106

@codingjoe codingjoe force-pushed the issues/14370 branch 7 times, most recently from 10bc3a2 to c183593 Compare April 3, 2016 16:37
@collinanderson
Copy link
Contributor

I might be able to try test this out later this week. Seems to me we should give this a release or two in the real world before deprecating raw_id_fields. Like, make sure it's good enough in practice to replace raw_id_fields.

@collinanderson
Copy link
Contributor

Does /foreignkey_json/ do permission checks? It seems to me it should require the same permissions as raw_id_fields (so if you're logged in to the admin, you can't just query that table unless you actually have permission, but maybe that's ok)

Edit: Actually, maybe we just need to make sure that you have the change permission for the Model that has the foreign key. That way it follows the permissions of a normal ChoiceField.

@codingjoe
Copy link
Sponsor Contributor Author

@collinanderson good point, that's what I thought. Lets see how it performs.
Regarding the permissions, you are correct, the change permission would the right thing to check. I'll put it on my list, thanks!

@guettli
Copy link
Contributor

guettli commented Apr 4, 2016

There is a small typo in the diff of docs/ref/models/fields.txt: you mean admin not admon.

@guettli
Copy link
Contributor

guettli commented Apr 4, 2016

@codingjoe first: Thank you for this great improvement. Next: It would be nice if the autocomplete would be easily re-usable outside the admin, too. Do you plan to support this?

@timgraham timgraham changed the title Resolves #14370 -- Adds select2 widget for foreign keys in admin Fixed #14370 -- Added select2 widget for foreign keys in admin. Apr 4, 2016
@codingjoe
Copy link
Sponsor Contributor Author

@guettli no, not really. We have django-select2 and django-autocomplete-light for that.
The really tricky part is to know which queryset to server as a JSON. django-select2 solves this by using the cache as a persistent storage shared by all application servers. In django-autocomplete-light you'll have to specify that explicitly.

I don't see a way to get this into core. This should remain a admin only feature for now, just like the raw ID field.

@collinanderson
Copy link
Contributor

@codingjoe btw, at some point we'll need to eventually get django-developers mailing list approval for vendoring select2.

Also, you've been mentioning raw_id_fields. I'm also hoping we can eventually use select2 to replace filter_horizontal/filter_vertical for m2m's.

@guettli
Copy link
Contributor

guettli commented Apr 5, 2016

@codingjoe you said:

The really tricky part is to know which queryset to server as a JSON

Yes, this is true, since the django admin interface needs a generic ajax server part.

But I still think it would be great to have a autocomplete component in django which can be used in django apps and the django admin.

You can make a BaseWidget available which gets subclassed once in the admin interface and once for the usage in custom apps.

I like small systems and having select2 twice in my static directory gives me a bad feeling. Yes it works and does not hurt, but somehow I think "less is more - avoid redundancy".

@jpic
Copy link
Contributor

jpic commented Apr 5, 2016 via email

Copy link
Member

@timgraham timgraham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This completes my detailed review of the patch:

  • Make sure a selenium test covers the changes in RelatedObjectLookups.js and all lines of autocomplete.js (e.g. as per Collin's comment, formset tests look missing).
  • test_select_multiple selenium test is erroring
  • I don't see any test failures if AutocompleteJsonView.paginate_by/get_paginator,and self.paginator_class=... are removed. Also, there aren't any tests that result in 'pagination': {'more': True}, I think.
  • test_search_use_distinct may be adding test coverage but it doesn't fail if qs = qs.distinct() is commented out as it should to be a good test.

return attrs

def optgroups(self, name, value, attr=None):
"""Return only selected options and set QuerySet from `ModelChoiceIterator`."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does "set QuerySet" mean?

in the drop-down.

``autocomplete_fields`` is a list of fields that you would like to change
to `Select2 <contrib-admin-select2>`_ autocomplete inputs.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This link is broken, I'm not sure what you intended to link to.

Copy link
Sponsor Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems to be old. I remove it.

:func:`ModelAdmin.get_search_results` implementation based on a
full text index search.

It's also advisable to change the ```Paginator`` on very large tables
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This leaves me wondering how to do that.

@@ -1329,8 +1329,9 @@ The possible values for :attr:`~ForeignKey.on_delete` are found in

If ``limit_choices_to`` is or returns a :class:`Q object
<django.db.models.Q>`, which is useful for :ref:`complex queries
<complex-lookups-with-q>`, then it will only have an effect on the choices
available in the admin when the field is not listed in
<complex-lookups-with-q>`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remind me why this change is related to this patch?

Copy link
Sponsor Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember writing this. I don't believe it's too. I will revert this.

@@ -957,3 +963,12 @@ def __str__(self):

class RelatedWithUUIDPKModel(models.Model):
parent = models.ForeignKey(ParentWithUUIDPK, on_delete=models.SET_NULL, null=True, blank=True)


class Author(models.Model):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are new models really needed here?

Copy link
Sponsor Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe so, yes. I can go ahead and alter other models more and more, eventually it lead to many side effects and will make the test suite hard to maintain. I removed as many models as I deemed unnecessary without making thing overly complex.

# We can not know if the field is an autocomplete field,
# since `get_autocomplete_fields` requires a request.
# The final url is therefore not available during the url setup.
resolve('/test_admin/admin/admin_views/answer/autocomplete/wrong_field/')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this test isn't working as expected -- it's resolving to RedirectView, same with test_inline_urls -- probably the result of the resolve should be checked.

Copy link
Sponsor Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe things moved around here, did you already fix it @timgraham ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this comment is still relevant. The comments at the "top" of this review that aren't connected to any specific lines are also still outstanding. Did you see those?

@codingjoe
Copy link
Sponsor Contributor Author

@timgraham I added a lot new selenium tests overing all the cases you described and all the cases I can think of.
While doing so I discovered and fix two bugs, including the one described by @collinanderson
Furthermore I added tests for the paginator and distinct behavior.

Puh, that took the the whole day, but it's done. Just in time, sorry for that :/

elem = self.selenium.find_element_by_css_selector('.select2-selection')
elem.click()
results = self.selenium.find_element_by_css_selector('.select2-results')
self.assertTrue(results.is_displayed())
elem = results.find_element_by_css_selector('.select2-results__option')
elem.click()

def test_inline_add(self):
self.selenium.get(self.live_server_url + reverse('admin:admin_views_question_add'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this test complete?

Copy link
Sponsor Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, just a leftover. I currently going over my changelog. I seem to have missed this. I'll remove it.


def test_select_multiple(self):
self.selenium.get(self.live_server_url + reverse('admin:admin_views_lecture_add'))
self.selenium.get(self.live_server_url + reverse('admin:admin_views_question_add'))
elem = self.selenium.find_element_by_css_selector('.select2-selection')
elem.click()
results = self.selenium.find_element_by_css_selector('.select2-results')
self.assertTrue(results.is_displayed())
elem = results.find_element_by_css_selector('.select2-results__option')
elem.click()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this test complete? What's the purpose of clicking on "No result found."?

Copy link
Sponsor Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does tests no results, which I didn't test in the test before. I will add it to the test before and test multiple select here in more detail including selecting many.

@codingjoe codingjoe force-pushed the issues/14370 branch 2 times, most recently from aed1a72 to c838620 Compare September 17, 2017 00:31
@codingjoe
Copy link
Sponsor Contributor Author

codingjoe commented Sep 18, 2017

@timgraham is this happening in 2.0? I'll watch my notifications closely today, incase anything comes up. But I am CEST.

…nd ManyToManyField in the admin.

Thanks Florian Apolloner and Tim Graham for review and
contributing to the patch.
@timgraham timgraham merged commit 94cd8ef into django:master Sep 18, 2017
@codingjoe codingjoe deleted the issues/14370 branch September 18, 2017 18:09
@auvipy
Copy link
Contributor

auvipy commented Sep 19, 2017

@codingjoe thanks for all your hard work and patience

<https://select2.org/>`_ autocomplete inputs.

By default, the admin uses a select-box interface (``<select>``) for fields
that are . Sometimes you don't want to incur the overhead of selecting all
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like not finished sentence

Copy link
Sponsor Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx :) see #9109

codingjoe added a commit to codingjoe/django that referenced this pull request Sep 19, 2017
Finished incomplete sentense in `autocomplete_fields`
documenation.

Ref django#6385
Ref django#14370
@codingjoe
Copy link
Sponsor Contributor Author

Thanks everyone for all the support, be it mental or though reviews. This has been a crazy but also amazing experience for me.

Thanks to @jpic and @apollo13 for your feedback and the help during the DjangoCon and DUTH sprints.

Special thanks to @kevin-brown who build the foundation this feature rests upon. I hope this will also send more contributors your way to make Select2 even better.

Last but not least, special thanks to @timgraham. Thank you for your guidance and thoroughness and seemingly limitless patience.

Thank you <3

@rom1dep
Copy link

rom1dep commented Sep 23, 2017

Hi @codingjoe , I just migrated my test project to django 2a1, and got to test your feature. Congrats for having it merged!
It looks nice on the admin model creation form, but I was somewhat expecting to see the same select2 control showing up in the filter pane (considering that I passed this field to both autocomplete_fields and list_filter). Do you think this could be considered as a nice to have addition?

For me, the whole point of this autocomplete kind of control is to better handle long lists of items, and list_filter has always been a pain for that. My current workaround is to use an approach like https://stackoverflow.com/a/39618385 , but it'd be much better to have it available out of the box, as part of django 2, now that select2 has been vendored :)

In any case, keep up the good work 👍

@collinanderson
Copy link
Contributor

@rom1dep you could propose something like that here: https://code.djangoproject.com/ticket/1873

@codingjoe
Copy link
Sponsor Contributor Author

@rom1dep love the idea! Lets put that into a separate ticket. Now that we have a version of Select2 vendored in Django's admin, it should be simple and not invoice too much discussion 👍

@paultiplady
Copy link
Contributor

Added this ticket to facilitate discussion - https://code.djangoproject.com/ticket/29111#ticket

@apollo13
Copy link
Member

apollo13 commented Feb 4, 2018

@paultiplady There isn't really much discussion needed (in the sense that we'd want that feature), if you feel up to the task a PR would be great!

@codingjoe
Copy link
Sponsor Contributor Author

I agree. Just build it, this would be an amazing addition and actually not too hard to implement.

@paultiplady
Copy link
Contributor

Good to know, thanks -- just wanted to check whether anyone else was already looking at it. I'm unlikely to have time in the next couple weeks but my team needs this feature so I'll hopefully find time to have a crack at it soon.

def test_has_change_permission_required(self):
"""
Users require the change permission for the related model to the
autocomplete view for it.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The requirement for the change permission means the autocomplete doesn't work for a user with only the new view permission on the related model.

See https://code.djangoproject.com/ticket/29502

This should probably be weakened. (No extra permission is needed here if you're not using autocomplete.)

@codingjoe: Your thoughts welcomed! 🙂

Copy link
Sponsor Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't even aware of the view permission. This is fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet