mlavin / django-selectable

Tools and widgets for using/creating auto-complete selection widgets using Django and jQuery UI.
http://django-selectable.readthedocs.io/en/latest/
BSD 2-Clause "Simplified" License
129 stars 64 forks source link

Simpler way to create Lookups #41

Closed mlavin closed 10 years ago

mlavin commented 10 years ago

Hello guys, thanks for the great app! Has been helping me a lot! Here's a scenario I couldn't think of a nice solution.

Problem is the following, I've got 10 fields that I want to be autocompleted and they're all the same logic. The user just types in a string, if he clicks save and it doesn't exist, it gets created. It's pretty much like the autocomplete our browsers have.

As they all have the same logic, I have a simple model for storing the autocomplete strings. Basically it stores a key (cellphone_number, email, etc) and it's value. So if I have the fields cellphone number, email, name, etc I'd have to have one class for each of these lookups, which is not optimal. As in the future I might have more fields, I wouldn't like to have 15 classes that do the same thing and only change the "key". I'm not sure if I explained it well enough. If you guys have any doubts, please ask!

It'd be sweet if I could create a new base class for these kinds of lookups and then I could just instantiate with the key. Something in the lines of:

#!python

email = forms.CharField( widget=selectable.AutoCompleteWidget(MySweetCustomLookup(key="email"))

Any idea if this is possible? MANY THANKS IN ADVANCE!


mlavin commented 10 years ago

I've resolved #42 so on tip you can now do

{{{ class MyClassForm(forms.ModelForm):

class Meta:
    model = MyClass
    widgets = {
        'email': selectable.AutoCompleteWidget(EmailLookup, allow_new=True, query_params={'key': 'email'}),
    }

}}}

and use the {{{ AutocompleteLookup }}} from my previous comment which pulls the {{{ key }}} from the GET. I'm resolving this issue. Let me know if you have additional questions or think of ways that would make this cleaner.


Original Comment By: Mark Lavin

mlavin commented 10 years ago

You currently can't pass the additional query parameters directly into the widget {{{init}}}. I've added #42 to allow for this in the future which would get about as close as possible to your original {{{email = forms.CharField( widget=selectable.AutoCompleteWidget(MySweetCustomLookup(key="email"))}}}.


Original Comment By: Mark Lavin

mlavin commented 10 years ago

Looking at this {{{AutoComplete}}} model I would say that you likely do not need to create new Lookup classes. You can instead look at adding additional query parameters: http://django-selectable.readthedocs.org/en/version-0.3.0/advanced.html#adding-parameters-on-the-server-side

In the form you would pass the additional query parameter

{{{ class MyClassForm(forms.ModelForm):

class Meta:
    model = MyClass
    widgets = {
        'email': selectable.AutoCompleteWidget(EmailLookup, allow_new=True),
    }

def __init__(self, *args, **kwargs):
    super(MyClassForm, self).__init__(*args, **kwargs)
    self.fields['email'].widget.update_query_parameters({'key': 'email'})

}}}

and in the lookup you would pull it from {{{request.GET}}} {{{ class AutocompleteLookup(ModelLookup): model = Autocomplete search_field = 'field_value__icontains'

def get_query(self, request, term):
    results = super(AutocompleteLookup, self).get_query(request, term)
    key = request.GET.get('key', '')
    if key:
        results = results.filter(field_name=key)
    return results

def create_item(self, value):
    return Autocomplete(field_name=self.key, field_value=value)

}}}

I'm not sure why that didn't come to mind yesterday when you were describing the problem but that seems like the cleanest solution.

Yes the {{{AutoCompleteWidget}}} does not create items it only returns text that would be the {{{AutoCompleteSelectField}}}. If you wanted to create this {{{Autocomplete}}} without using a FK you could instead use a {{{post_save}}} signal on the {{{MyClass}}}: https://docs.djangoproject.com/en/1.3/topics/signals/


Original Comment By: Mark Lavin

mlavin commented 10 years ago

Hi Mark, thanks for the swift reply!! Thanks for the snippet! It's almost what I needed. I think I didn't explain something correctly. There are actually only 2 fields (field_name and field_value) so the table looks something like this:

{{{

!python

field_name field_value email me@me.com email lol@lol.com email ohwow@google.com phone 3455676 name Mark Lavin name Bernardo Pires phone 45991019 }}}

So here is how my lookup class looks like right now:

{{{

!python

class EmailLookup(ModelLookup): model = Autocomplete search_field = 'field_value__icontains' key = 'email'

def get_queryset(self):
    return Autocomplete.objects.filter(field_name=self.key)

def create_item(self, value):
    return Autocomplete(field_name=self.key, field_value=value)

}}} The only thing that changes between these classes is the key parameter. Do you have maybe any idea how to do it better? When not, it's also ok, thanks a lot for your help! Your suggestion about separate classes do make sense, considering I might have additional customization.

Another question: Am I doing something wrong or simply using the widget AutoCompleteWidget does not create new items? Because right now I have form like this:

{{{

!python

class Meta: model = MyClass widgets = { 'email': selectable.AutoCompleteWidget(EmailLookup, allow_new=True), } }}}

{{{

!python

class MyClass(models.Model) email = models.CharField(max_length=200, blank=True) }}}

This works perfectly to get the autocomplete items, but it never saves the item if it doesn't exist. Do I have to use the AutoCompleteSelectField? This would then mean that I'd have to change my model to have a ForeignKey to autocomplete which was something that I was looking to avoid. Any ideas?

Many many thanks in advance! Keep up the great work!


Original Comment By: Bernardo Pires

mlavin commented 10 years ago

Thank you for the kind words. Currently there is a problem with passing arguments into the {{{init}}} of the lookup class since the lookup class is also instantiated in the view where these arguments are not know. Withoug significant changes to those internals that means you need to create separate classes for each lookup or find a clever way to use the same lookup. However, this is still just Python so you can construct classes at runtime.

{{{ from selectable.base import ModelLookup from selectable.registry import registry

from myapp.models import MyModel

mylookups = {} #Used to store key --> lookup class keys = ['email', 'phone', 'name', ] for key in keys: mylookups[key] = type( '%sLookup' % key.title(), (LookupBase, ), {'model': MyModel, 'serach_fields': ('%s__icontains' % key, ), } ) registry.register(mylookups[key]) }}}

I haven't tested the above code but I don't see any obvious reason why it wouldn't work. You could then pull out the class from the {{{ mylookups }}} dictionary to pass into the widget. While it's less lines of code than creating separate classes but it isn't particularly readable. Honestly I would say write out the separate classes. You only have to do it once and it would allow you to write additional customization to each lookup (like fuzzy phone number matching or limiting emails by domain) in the future.


Original Comment By: Mark Lavin