Making a Subway Sandwich

My Django app takes orders from users and creates a group order. Each order is comprised of a sandwich. One of the fields is a ManyToManyField for a multi selection of vegetables. Lettuce, cucumbers, onions, etc. I would like to have the ability to add more vegetables later on in the admin section. I can see the list of vegetables currently in the admin section. The order, which has the multiple select field is there, I am able to save selections. However the form submission works for every other item except the veggie’s.

class SubwayVeggie(models.Model):
  veggie = models.CharField(max_length=100, unique=True)

  def __str__(self):
  return self.veggie

class SubwayOrder(models.Model):

class SubwayForm(ModelForm):
  class Meta: 
    model = SubwayOrder
    fields = ( 'veggie',)
labels = { 'veggie':'Veggies (Multiple)',}
widgets = {'veggie':forms.CheckboxSelectMultiple(attrs={'class':'check-input'}),}

Questions:

  1. Is the widget setting correct? I’m using {{ form }} in the html. The entire form is visible. I can select individual values of veggies. No selections are being shown as selected in the admin section. Other items selected like ForeignKeys are being shown as selected in the order.

  2. Is the ManyToManyField the correct field to select multiple items?

  3. Assuming I get the ManyToManyField to work, how can I change the admin view of the items to radio’s. Is there a way?

Any help at all would be greatly appreciated. Very frustrated.

Best Regards.

-=Mike=-

First, side note - when posting code here, please enclose the code between lines of three backtick - ` characters. This means you’ll have a line of ```, then your code, then another line of ```. This ensures your code stays properly formatted and special character sequences don’t get interpreted. (If you are inclined to do so, I’d encourage you to edit your original post and insert those lines of ``` before and after your code.)

I’m a bit confused by your use of the phrases admin section and admin view. Typically, when someone refers to the admin, they’re referring to Django’s admin app. However, you’re making references to a form that you’ve created along with "using {{ form }} in the html.
Are you creating your own views and templates here, or are you relying upon Django’s admin?

If you’re creating your own view, we’ll need to see that view to determine if you’re handling the ManyToMany relationship correctly.

If you’re relying upon Django’s admin facility, we’ll need to see your ModelAdmin class.

Yes, a ManyToMany field would be an appropriate way to model multiple veggies being related to multiple sandwiches.

Finally, a Radio button is the wrong widget for a multi-selection option. Radio buttons are designed as a “pick 1 from this list” interface, not “pick 0 or more”. Your appropriate choices would include a multiselect box, a set of checkboxes, or a “two column” selection widget.

Thank-you Ken for the quick response. I’ve been working solo on this project and have hit the proverbial wall. Getting a quick response gives me hope that I can get this problem fixed. Enough of that but, again thanks.

I have edited the code with the backticks and will continue to do so. I had no idea, obviously.

I refer to admin section and admin view incorrectly. To clarify, I’m referring to the admin section/area not a view. My question regarding the admin section is this: is it possible to have radio buttons for multiple selects instead of the list control. I tried what I thought was a solution but couldn’t get it to work.
Please confirm if you understand what I’m asking and if the link below is the correct method to use to get radio buttons working in the admin section.

https://docs.djangoproject.com/en/3.2/ref/contrib/admin/#working-with-many-to-many-models

image

“Are you creating your own views and templates here, or are you relying upon Django’s admin?”
Yes, for both.
Template:

{% extends 'mealapp/base.html' %}
{% block content %}
  <h1>Subway Menu Order</h1>
  <br/><br/>
  {% if submitted %}
    Hey {{ user.first_name }}, your order was Submitted Successfully!!

  {% else %}

  <form action="" method=POST>
    {% csrf_token %}
    
    <strong>{{ user.first_name }} {{ user.last_name }} (user): {{user}}</strong> 
    <br>
    {{ form }}
      
    
    <input type="submit" value="Submit" class="btn btn-primary">
      
  </form>
  {% endif %}

{% endblock %}

views.py

# ───────────────────────────────────────────────────────────────────────────────
# Subway menu form selections. This is used by the user to submit a subway
# order.
# ───────────────────────────────────────────────────────────────────────────────
@login_required(login_url='/login')
def subway_menu(request):
  # First time user gets here submitted is False
  submitted = False
  if request.method == "POST":
      form = SubwayForm(request.POST)
      if form.is_valid():
        # form.save() standard way.
        instance = form.save(commit=False)
        # Attach user_name to instance to store the user submitting the order.
        instance.user_name = request.user
        # user_name = request.user
        print(instance.user_name)
        # Save meal to Database with user_name
        instance.save()
        return HttpResponseRedirect('/meals/subway_menu?submitted=True')
  else:
    # Define the form for the render below.
    form = SubwayForm
    if 'submitted' in request.GET: 
        submitted = True
  
  return render(request, 'meals/subway_menu.html', {'form': form, 'submitted': submitted})

forms.py

# ───────────────────────────────────────────────────────────────────────────────
# Subway Order Form
# ───────────────────────────────────────────────────────────────────────────────
class SubwayForm(ModelForm):
  class Meta: 
    model = SubwayOrder
    fields = (
      'bread',
      'sandwich',
      'toasted',
      'bread_size',
      'cheese',
      'veggie',
    )
    # Not sure if I need labels if I'm styling my own .css
    labels = {
      'bread':'Choose your Bread', 
      'sandwich':'All Day Sandwiches',
      'bread_size':'Bread Size (12")', 
      'toasted':'Toasted (No)',
      'cheese':'Cheese',
      'veggie':'Veggies (Multiple)',
      
    }
    
   # ───────────────────────────────────────────────────────────────────────────────
    # Designate a widgets dictionary for styling with boostrap
    widgets = {
      'bread': forms.RadioSelect(attrs={'class':'check-input'}), 
      'sandwich':forms.RadioSelect(attrs={'class':'check-input'}), 
      'bread_size': forms.RadioSelect(attrs={'class':'check-input'}),
      'toasted':forms.RadioSelect(attrs={'class':'check-input'}), 
      'cheese':forms.CheckboxSelectMultiple(attrs={'class':'check-input'}),
      'extra':forms.CheckboxSelectMultiple(attrs={'class':'check-input'}),
      
    }

Hi Ken,

Testing to see if you get this, in part.

I’ve responded to your latest post regarding my question. The post is being withheld by the Akismet temporarily to check that it’s not spam.

Hope to see it posted soon.

Best regards,

-=Mike=-

I have not got a response back from the Django admins regarding my blocked first response to your post. So I’m posting over again.

I’m not using forms.RadioSelect for my multi select widgets. I’m using forms.CheckboxSelectMultiple with models.ManyToManyField(SubwayVeggie). I assume you mean tuples when you say “two column” select. I’m using those for other fields and they work fine for small lists. But I need to use the ManyToMay for larger lists and be able to maintain the list in the admin section. However I can’t find any documentation that explains what I’m looking to do.

  1. Show the multiple selections in the subwayOrder. In the veggies field see a comma separated string of the values selected in the veggies column.

  2. Show the values in the admin section using radio buttons rather than the default list.

  3. Have the values selected be posted with the rest of the selections. I can do this with single selections just not with multiple.

@login_required(login_url='/login')
def subway_menu(request):
  # First time user gets here submitted is False
  submitted = False
  if request.method == "POST":
      form = SubwayForm(request.POST)
      if form.is_valid():
        # form.save() standard way.
        instance = form.save(commit=False)
        # Attach user_name to instance to store the user submitting the order.
        instance.user_name = request.user
        # user_name = request.user
        print(instance.user_name)
        # Save meal to Database with user_name
        instance.save()
        return HttpResponseRedirect('/meals/subway_menu?submitted=True')
  else:
    # Define the for for the render below.
    form = SubwayForm
    if 'submitted' in request.GET: 
        submitted = True
  
  return render(request, 'meals/subway_menu.html', {'form': form, 'submitted': submitted})

models.py

# ───────────────────────────────────────────────────────────────────────────────
# Subway Subway Orders
# ───────────────────────────────────────────────────────────────────────────────
class SubwayOrder(models.Model):
  
  # first_name = User.first_name

  user_name = models.ForeignKey(User, 
  on_delete=models.CASCADE, 
  default=None, 
  null=True, 
  blank=True)
    
  bread = models.ForeignKey(SubwayBread, default=13, on_delete=models.CASCADE)
  order_Submitted_Date = models.DateTimeField(auto_now=True)
  sandwich = models.ForeignKey(SubwaySandwich, default=0, on_delete=models.CASCADE)
  BREAD_SIZE = (
    ('12"','12"'),
    ('6"','6"'),
  )
  bread_size = models.CharField(max_length=10, choices=BREAD_SIZE, default='12"')
  
  TOASTED = (
    ('Yes', 'Yes'),
    ('No', 'No'),
  )
  toasted = models.CharField(max_length=10, choices=TOASTED, default='No')
  
  # cheese references the CharField cheese above. 
  # null = False (default) NO empty values allowed.
  # blank = False (default) NO blank values allowed.
  # required = True (default)
  cheese = models.ManyToManyField(SubwayCheese)
  # cheese = models.ForeignKey(SubwayCheese, default=1, on_delete=models.CASCADE)
  # extra = models.ManyToManyField(SubwayExtra)
  veggie = models.ManyToManyField(SubwayVeggie)
  # veggie = models.ForeignKey(SubwayVeggie, default=1, on_delete=models.CASCADE)
  # sauce = models.ForeignKey(SubwaySauce, default=1, on_delete=models.CASCADE)
  # drink = models.ForeignKey(SubwayDrink, default=1, on_delete=models.CASCADE)
  # cookie = models.ForeignKey(SubwayCookie, default=1, on_delete=models.CASCADE)
  # chips = models.ForeignKey(SubwayChip, default=1, on_delete=models.CASCADE)
   
  # Don't know what the hell to return here.

  def __int__(self):
    return self.bread

# ───────────────────────────────────────────────────────────────────────────────

admin.py

# ───────────────────────────────────────────────────────────────────────────────
# Subway Orders
# ───────────────────────────────────────────────────────────────────────────────
@admin.register(SubwayOrder)
class SubwayOrder(admin.ModelAdmin):
  list_display = (
  'user_name',
  # 'first_name',
  'bread',
  'order_Submitted_Date',
  'sandwich',
  'toasted',
  'bread_size',
  # 'cheese',
  # 'extra', 
  # 'veggie', # Required a custom method.
  # 'sauce',
  # 'drink',
  # 'cookie',
  # 'chips',
  )
  
  # ordering = ('bread_size',)
  # search_fields = ('toasted',)
  radio_fields = {
    'bread':admin.VERTICAL,
    'sandwich':admin.VERTICAL,
    'toasted':admin.VERTICAL,
    # 'cheese':admin.VERTICAL,
    'bread_size':admin.VERTICAL,
    # 'extra':admin.VERTICAL,
    # 'veggie':admin.VERTICAL,
  }

Best regards,

-=Mike=-

No, I’m talking about the two-column JavaScript widget that the Django Admin uses for group & permission assignments to the User object. (See image below)

Accessing the entries from a M2M are described in the Many-to-many relationships page in the docs. You can retrieve all the veggies associated with an order and format it.

This is what I’m referring to as not being an appropriate use of Radio buttons for a UI.

See the section on the side effects of using commit=False with models having many-to-many relationships with other models in the save method docs.

Image referenced above of the Django Admin “two column” multi-select widget.

Thanks again for the quick response.
I’m using commit=False to get the user instance and save the user to the order. There’s probably another way to do this but this method works. I’d be happy to hear any thoughts you have on other way’s to capture the user.

Thanks for directing me to the form.save_m2m() I missed that, obviously. Problem solved.

I don’t quite follow why I can’t use a radio button on the multi selects in Django admin. Seems like that method would be a popular choice. I’ll keep reading on the use of the two column multi-select widget implementation.

Thanks again Ken.

-=Mike=-

No, that’s the right way to do it - I was just pointing out the side effect of doing it that makes it necessary for you to call save_m2m.

A Radio button is a “select one of” widget, not a “select 0 or more of” widgets. You can’t have a “multiselect” of a radio button group. A radio button group allows the selection of one item. Unless you’ve got some other idea in mind, I don’t see where it fits the requirements.