Django Autocomplete Light create new choice
Asked Answered
S

3

7

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

Staphyloplasty answered 17/11, 2018 at 16:29 Comment(0)
S
7

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()
Staphyloplasty answered 18/11, 2018 at 14:4 Comment(0)
B
1

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

Boom answered 20/11, 2018 at 16:35 Comment(2)
You're right, that was one of the issues. However the main problem was that I didn't realise I had to include the get_create_option methodStaphyloplasty
I don't think so.Boom
S
1

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.

Salian answered 18/9, 2019 at 10:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.