Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django Autocomplete Light create new choice

I have been working through the following tutorial provided for Django Autocomplete Light:

https://django-autocomplete-light.readthedocs.io/en/master/tutorial.html

I have successfully implemented autocompletion for one of the fields in my form, however I am unable to complete the following section:

https://django-autocomplete-light.readthedocs.io/en/master/tutorial.html#creation-of-new-choices-in-the-autocomplete-form

The documentation states that I should be able to add in a feature which allows the user to create a new choice in the form if their required choice is unavailable. However the tutorial is not particularly clear in explaining how to do this.

I am trying to implement a form in which the user can create a new Feedback by:

  1. Selecting from an autocompleting list of Categories
  2. Selecting a Message corresponding to the chosen Category
  3. If the Category or Message they wish to choose is not available, they should be able to add to the existing choices

I have this partly implemented, but it does not appear to work correctly as if no Category is selected, the drop down for the Messages displays the list of Categories. However, if a Category is selected, the correct Messages are displayed as required.

models.py

class Feedback(models.Model):
     feedback_id = models.IntegerField(primary_key=True,default=0)
     pre_defined_message = models.ForeignKey('Message',on_delete=models.CASCADE,null=True,blank=True) # Selected from a pre defined list depending on selected category
     points = models.IntegerField(default=0)
     lecturer = models.ForeignKey('LecturerProfile', on_delete=models.CASCADE, null=True, blank=True)
     student = models.ForeignKey('StudentProfile', on_delete=models.CASCADE, null=True, blank=True)
     which_course = models.ForeignKey('Course', on_delete=models.CASCADE, null=True, blank=True)
     datetime_given = models.DateTimeField(default=timezone.now, blank=False)
     optional_message = models.CharField(max_length=200,default="")
     category = models.ForeignKey('Category', on_delete=models.CASCADE, null=True, blank=True)

 class Category(models.Model):
     name = models.CharField(max_length=20, default="Empty",primary_key=True)

     def __str__(self):
         return self.name

class Message(models.Model):
     category = models.ForeignKey('Category',on_delete=models.CASCADE,null=True,blank=True)
     text = models.CharField(max_length=200,default="No message",primary_key=True)

     def __str__(self):
          return self.text

forms.py

class FeedbackForm(autocomplete.FutureModelForm):
     optional_message = forms.CharField(max_length=200, required=False)

     class Meta:
         model = Feedback
         fields = ('category', 'pre_defined_message','optional_message','points')
         widgets = {
             'pre_defined_message': autocomplete.ModelSelect2(url='category_autocomplete',forward=['category']),
             'category': autocomplete.ModelSelect2(url='category_autocomplete')
         }
         help_texts = {
             'pre_defined_message': "Select a Message",
             'category': 'Category',
             'optional_message': "Optional Message",
             'points': "Points"
         }

views.py

class CategoryAutocomplete(autocomplete.Select2QuerySetView):
     def get_queryset(self):
         if not self.request.user.is_authenticated or not self.request.user.is_lecturer:
             return Category.objects.none()

         query_set = Category.objects.all()

         category = self.forwarded.get('category', None)

         if self.q:
             query_set = query_set.filter(name__istartswith=self.q)
             return query_set

         if category:
             query_set = Message.objects.filter(category=category)

         return query_set

urls.py

re_path(r'^category-autocomplete/$', CategoryAutocomplete.as_view(create_field='name'), name='category_autocomplete'),


I have searched for an answer to this for a while and have struggled to find a solution. I am also aware that my forms.py in particular may not have the most efficient/clean code and am open to suggestions to improve this. I have tried defining an init method however I was unable to do this successfully.

Thanks in advance

like image 369
Ashwin M Avatar asked Nov 17 '18 16:11

Ashwin M


People also ask

Which version of Django is used for Autocomplete?

Python 2.7, 3.4, Django 2.0+ support (Django 1.11 (LTS), is supported until django-autocomplete-light-3.2.10), Django generic many to many relation support (through django-generic-m2m and django-gm2m) Multiple widget support: select2.js, easy to add more. Offering choices that depend on other fields in the form, in an elegant and innovative way,

How to add custom functions in Django autocomplete light?

A widget is dynamically added, i.e. with formsets. This is handled by autocomplete_light.js, which is going to trigger an event called dal-init-function on the document when Django Autocomplete Light has initialized. At this point you can simply call yl.registerFunction () to register your custom function.

Which version of Python is used for Autocomplete?

Python 2.7, 3.4, Django 2.0+ support (Django 1.11 (LTS), is supported until django-autocomplete-light-3.2.10), Multiple widget support: select2.js, easy to add more.

What is the second argument Func in autocomplete_light?

The second argument func is the callback function to be run by Django Autocomplete Light when it initializes your input autocomplete. autocomplete_light.js also keeps track of initialized elements to prevent double-initialization.


3 Answers

After searching through all the open source documentation of Django Autocomplete Light:

https://github.com/yourlabs/django-autocomplete-light

I believe I have found a solution to this and thought I should share it for others confused by the provided tutorial.

After reaching the stage that I have above (i.e working autocompletion) you must include a get_create_option method to allow the view to understand what to do when it retrieves a create_field.

So in urlpatterns list in urls.py ensure the following line is present:

re_path(r'^category-autocomplete/$', CategoryAutocomplete.as_view(model=Category,create_field='name'), name='category_autocomplete')


(Note: the create_field variable must be set to the primary key of the relevant model. In my case, the primary key of Category model is name)

What is not made clear in the tutorial is the next step. After looking in the following file:

https://github.com/yourlabs/django-autocomplete-light/blob/master/src/dal_select2/views.py

I found a method get_create_option which handles the creation of the new option.

def get_create_option(self, context, q):
    """Form the correct create_option to append to results."""
    create_option = []
    display_create_option = False
    if self.create_field and q:
        page_obj = context.get('page_obj', None)
        if page_obj is None or page_obj.number == 1:
            display_create_option = True

        # Don't offer to create a new option if a
        # case-insensitive) identical one already exists
        existing_options = (self.get_result_label(result).lower()
                            for result in context['object_list'])
        if q.lower() in existing_options:
            display_create_option = False

    if display_create_option and self.has_add_permission(self.request):
        create_option = [{
            'id': q,
            'text': _('Create "%(new_value)s"') % {'new_value': q},
            'create_id': True,
        }]
    return create_option


After including this method in my CategoryAutocomplete class in my views.py, the ability to create a new Category within the search finally worked!

I am now having difficulty creating a Message object with the previously selected Category as a foreign key as this is also not well documented. I will update this answer if I find a solution.

Hopefully this is of some help to someone!

UPDATE

Although it is a bit of a hack, I have managed to set the foreign key of the Message model. I simply access the created Message and set its category field within the form validation itself:

if request.method == 'POST':
        form = FeedbackForm(request.POST)
        if form.is_valid():
            new_fb = form.save(commit=False)
            # When a new message is made, the category it is associated with is not saved
            # To fix this, set the category field within this form and save the message object.
            new_fb.pre_defined_message.category = Category.objects.get(name=new_fb.category)
            new_fb.pre_defined_message.save()
like image 119
Ashwin M Avatar answered Oct 13 '22 00:10

Ashwin M


Maybe the problem is that the user doesn't have the add permission that get_create_option checks ?

Does it work if you add this to your view ?

def has_add_permission(self, request): return True

like image 43
jpic Avatar answered Oct 13 '22 00:10

jpic


I have a model

 class sites(models.Model): #Site Owner for standard sites will be system_1
    site = models.CharField(max_length=100)
    site_owner = models.ForeignKey(User, on_delete=models.CASCADE, blank = True, null=True)
    def __str__(self):
        return self.site

I want users to be able to add new sites via autocomplete and to also record which user has created a site

in dal\views.py - class BaseQuerySetView(ViewMixin, BaseListView): there is following

def create_object(self, text):
    """Create an object given a text."""
    return self.get_queryset().get_or_create(
        **{self.create_field: text,})[0]

So I overrode this in my autocomplete class in my views with

def create_object(self, text):
    """Create an object given a text."""        
    return self.get_queryset().get_or_create(site_owner=self.request.user,
        site=text)[0]

This could be extended further to then updated multiple rows in a model if required & in your case you should be able to pass the previously selected Category into this def.

like image 1
Nic_D Avatar answered Oct 12 '22 22:10

Nic_D