pythondjangodjango-viewsdjango-templatesdjango-select2

AttributeError with ModelSelect2Widget


I installed Django-Select2 and applied it to my project, but I'm having an error that I can't wrap my head around.

Here is my forms.py file:

from django.forms import modelform_factory
from django_select2 import forms as s2forms
from .models import Employer, JobListing, Category_JobListing

class LocationWidget(s2forms.ModelSelect2Widget):
    search_fields = [
        "location__icontains",
    ]

    def get_queryset(self):
        return Employer._meta.get_field("location").choices


EmployerForm = modelform_factory(Employer, fields=["name", "location", "short_bio", "website", "profile_picture"], widgets={"location": LocationWidget})

views.py file (only the relevant code parts):

def submitJobListing(request):
    if request.method == "POST":
        employer_form = EmployerForm(request.POST, request.FILES)
        employer = employer_form.save()
        return HttpResponse("Your form has been successfully submitted.")

    else:
        employer_form = EmployerForm()

    context = {
        "employer_form": employer_form,
    }

    return render(request, 'employers/submit_job_listing.html', context)

submit_job_listing.html (my template file):

<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ employer_form }}
    <input type="submit" value= "Submit">
</form>

With this code, I get:

AttributeError at /submit-job-listing/

'list' object has no attribute 'none'

The output says that the error is related to {{ employer_form }} in the template and return render(request, 'employers/submit_job_listing.html', context) in the view.

I know that this error occurs because I have overridden the get_queryset() method in the LocationWidget, but I can't figure out what I'm doing wrong.

What is wrong here and how do I fix it?

Update 1:

I get an issue saying NoReverseMatch at /submit-job-listing/ Reverse for 'location_choices' not found. 'location_choices' is not a valid view function or pattern name.. This is most likely an issue with the integration of the answer's code within my overall project. I don't understand why it is happening. The URL I'm trying to access when I get the error is http://127.0.0.1:8000/submit-job-listing/.

Here is the relevant code:

forms.py:

from django.forms import modelform_factory
from django_select2 import forms as s2forms
from .models import Employer, JobListing, Category
from django import forms

class LocationWidget(s2forms.HeavySelect2Widget):
    data_view = "location_choices"

    def __init__(self, attrs=None, choices=(), **kwargs):
        super().__init__(attrs=attrs, choices=choices, data_view=self.data_view, **kwargs)

EmployerForm = modelform_factory(Employer, fields=["name", "location", "short_bio", "website", "profile_picture"], widgets={"location": LocationWidget})
JobListingForm = modelform_factory(JobListing, fields=["job_title", "job_description", "job_requirements", "what_we_offer", "job_application_url", "categories"])

urls.py:

from django.urls import path

from . import views

app_name = "employers"
urlpatterns = [
    path("", views.ListJobListingsView.as_view(), name="list_joblistings"),
    path("choices/location.json", views.LocationChoicesView.as_view(), name="location_choices"),
    path("<int:pk>/", views.DetailJobListingView.as_view(), name="detail_joblisting"),
    # path("employers/", views.IndexEmployersView.as_view(), name="index"),
    path("employers/<int:pk>/", views.DetailEmployerView.as_view(), name="detail_employer"),
    path("submit-job-listing/", views.submitJobListing, name="submit_job_listing"),
    # path("submit-job-listing/success/", )
]

views.py:

def submitJobListing(request):
    if request.method == "POST":
        employer_form = EmployerForm(request.POST, request.FILES)
        job_listing_form = JobListingForm(request.POST, request.FILES)
        #check if employer with that name already exists
        employer_name = str(request.POST.get("name", ""))
        employer_with_the_same_name = Employer.objects.get(name=employer_name)
        employer = None
        if (employer_with_the_same_name != None):
            employer = employer_with_the_same_name

        if employer == None and employer_form.is_valid() and job_listing_form.is_valid():
            employer = employer_form.save()

        job_listing = job_listing_form.save(commit=False)
        job_listing.employer = employer
        job_listing.save()

        #return HttpResponseRedirect(reverse("employers:thank_you")) # TODO: Add this URL and template
        return HttpResponse("Your form has been successfully submitted.")

    else:
        employer_form = EmployerForm()
        job_listing_form = JobListingForm()

    context = {
        "employer_form": employer_form,
        "job_listing_form": job_listing_form,
    }

    return render(request, 'employers/submit_job_listing.html', context)

# https://stackoverflow.com/questions/64425630/attributeerror-after-i-added-select2-as-a-widget-in-one-of-my-forms-how-to-fix/64618891?noredirect=1#comment114273660_64618891
class ChoicesView(generic.list.BaseListView):
    paginate_by = 25

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.object_list = None
        self.term = None

    def get(self, request, *args, **kwargs):
        self.term = kwargs.get("term", request.GET.get("term", ""))
        self.object_list = self.get_queryset()
        context = self.get_context_data()
        return JsonResponse(
            {
                "results": [
                    {"id": value, "text": name}
                    for value, name in context["object_list"]
                ],
                "more": context["page_obj"].has_next(),
            }
        )

    def get_queryset(self):
        return [(value, name) for value, name in self.queryset if self.term.lower() in name.lower()]


class LocationChoicesView(ChoicesView):
    queryset = COUNTRY_CITY_CHOICES

Traceback:

Environment:


Request Method: GET
Request URL: http://127.0.0.1:8000/submit-job-listing/

Django Version: 3.1.2
Python Version: 3.8.3
Installed Applications:
['employers.apps.EmployersConfig',
 'django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'django_select2']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware']


Template error:
In template /home/john/Documents/Django_projects/jobsforjuniorseu/Version_1/jobsforjuniorseu/employers/templates/employers/submit_job_listing.html, error at line 15
   Reverse for 'location_choices' not found. 'location_choices' is not a valid view function or pattern name.
   5 :     {{ employer_form.media.css }}
   6 :     <style>
   7 :         input, select {width: 100%}
   8 :     </style>
   9 : </head>
   10 : <body>
   11 :     <h1>Submit a job listing</h1>
   12 :     <form method="post" enctype="multipart/form-data">
   13 :         {% csrf_token %}
   14 :         {{ job_listing_form }}
   15 :          {{ employer_form }} 
   16 :         <input type="submit" value= "Submit">
   17 :     </form>
   18 :     <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
   19 :     {{ employer_form.media.js }}
   20 : </body>
   21 : </html>
   22 : 

Traceback (most recent call last):
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/core/handlers/base.py", line 179, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/home/john/Documents/Django_projects/jobsforjuniorseu/Version_1/jobsforjuniorseu/employers/views.py", line 88, in submitJobListing
    return render(request, 'employers/submit_job_listing.html', context)
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/shortcuts.py", line 19, in render
    content = loader.render_to_string(template_name, context, request, using=using)
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/template/loader.py", line 62, in render_to_string
    return template.render(context, request)
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/template/backends/django.py", line 61, in render
    return self.template.render(context)
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/template/base.py", line 170, in render
    return self._render(context)
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/template/base.py", line 162, in _render
    return self.nodelist.render(context)
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/template/base.py", line 938, in render
    bit = node.render_annotated(context)
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/template/base.py", line 905, in render_annotated
    return self.render(context)
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/template/base.py", line 994, in render
    return render_value_in_context(output, context)
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/template/base.py", line 973, in render_value_in_context
    value = str(value)
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/utils/html.py", line 376, in <lambda>
    klass.__str__ = lambda self: mark_safe(klass_str(self))
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/forms/forms.py", line 134, in __str__
    return self.as_table()
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/forms/forms.py", line 272, in as_table
    return self._html_output(
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/forms/forms.py", line 229, in _html_output
    output.append(normal_row % {
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/utils/html.py", line 376, in <lambda>
    klass.__str__ = lambda self: mark_safe(klass_str(self))
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/forms/boundfield.py", line 34, in __str__
    return self.as_widget()
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/forms/boundfield.py", line 93, in as_widget
    return widget.render(
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django_select2/forms.py", line 262, in render
    output = super().render(*args, **kwargs)
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/forms/widgets.py", line 241, in render
    context = self.get_context(name, value, attrs)
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/forms/widgets.py", line 678, in get_context
    context = super().get_context(name, value, attrs)
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/forms/widgets.py", line 638, in get_context
    context = super().get_context(name, value, attrs)
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/forms/widgets.py", line 234, in get_context
    'attrs': self.build_attrs(self.attrs, attrs),
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django_select2/forms.py", line 240, in build_attrs
    "data-ajax--url": self.get_url(),
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django_select2/forms.py", line 235, in get_url
    return reverse(self.data_view)
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/urls/base.py", line 87, in reverse
    return iri_to_uri(resolver._reverse_with_prefix(view, prefix, *args, **kwargs))
  File "/home/john/anaconda3/envs/django/lib/python3.8/site-packages/django/urls/resolvers.py", line 685, in _reverse_with_prefix
    raise NoReverseMatch(msg)

Exception Type: NoReverseMatch at /submit-job-listing/
Exception Value: Reverse for 'location_choices' not found. 'location_choices' is not a valid view function or pattern name.

Solution

  • It looks like you just want a light1 widget.

    If choices is a large static list, then you may want a heavy widget.

    Light widget

    Replace LocationWidget with s2forms.Select2Widget.

    forms.py:

    EmployerForm = modelform_factory(
        Employer,
        fields=["name", "location", "short_bio", "website"],
        widgets={"location": s2forms.Select2Widget})
    

    Heavy widget

    Instead of inheriting s2forms.ModelSelect2Widget for LocationWidget:

    forms.py:

    from django.db.models.fields import BLANK_CHOICE_DASH
    
    
    class LocationWidget(s2forms.HeavySelect2Widget):
        data_view = "location_choices"  # "employers:location_choices"
    
        def __init__(self, attrs=None, choices=(), **kwargs):
            super().__init__(attrs=attrs, choices=choices, data_view=self.data_view, **kwargs)
    
        @property
        def choices(self):
            return BLANK_CHOICE_DASH  # Avoid rendering all choices in HeavySelect2Widget
    
        @choices.setter
        def choices(self, value):
            pass
    

    urls.py:

    # app_name = "employers"
    urlpatterns = [
        path('', views.submitEmployer, name='index'),
        path('choices/location.json', LocationChoicesView.as_view(), name='location_choices'),
    ]
    

    views.py:

    class ChoicesView(BaseListView):
        paginate_by = 25
    
        def __init__(self, **kwargs):
            super().__init__(**kwargs)
            self.object_list = None
            self.term = None
    
        def get(self, request, *args, **kwargs):
            self.term = kwargs.get("term", request.GET.get("term", ""))
            self.object_list = self.get_queryset()
            context = self.get_context_data()
            return JsonResponse(
                {
                    "results": [
                        {"id": value, "text": name}
                        for value, name in context["object_list"]
                    ],
                    "more": context["page_obj"].has_next(),
                }
            )
    
        def get_queryset(self):
            return [(value, name) for value, name in self.queryset if self.term.lower() in name.lower()]
    
    
    class LocationChoicesView(ChoicesView):
        queryset = COUNTRY_CITY_CHOICES
    

    References

    1. Widgets: https://django-select2.readthedocs.io/en/latest/django_select2.html#widgets
    2. The Select2 data format: https://select2.org/data-sources/formats
    3. Ajax (remote data): https://select2.org/data-sources/ajax