"Business is not iterable" error returning data on same HTML page as form

Question:

I can get data from the form into database and pass that to the view where it queries Yelp and puts it into a JSON file, then particular fields from the JSON file are saved to database. But I can’t display the database data.

I get the search success message when I should be returning the data to the page. I found my dict is only one piece of data instead of all fields of the row and I am not sure why :

{% for key, values in yelp_data.items %}
<tr>
    <td>{{key}}</td>
    {% for v in values %}
    <td>{{v}}</td>
    {% endfor %}
</tr>
    {% endfor %}

I get "Business is not iterable" error.

index.html

...
<div class="row">
    <div class="col-md-4">
        <h2>Getting started</h2>
        <p>
            Click here to begin with Yelp!
        </p>
        <p><a class="btn btn-default" href="yelping">Yelp! &raquo;</a></p>
    </div>
...

views.py

...
@csrf_exempt
def yelping(request):
    if request.method == "POST":
        form = YelpForm(request.POST or None)
        if form.is_valid():
            form.save(commit=False)
            term = request.POST['term']
            location = request.POST['location']
            form.save()
            print("yelping", term, location)
            yelp_main(request)
            messages.success(request, "Search successful." )
            return render(request, 'app/yelp.html', {'form' : form})
        else:
            form = YelpForm()
        messages.error(request, "Unsuccessful Search. Invalid information.")
        form = YelpForm()
        
    assert isinstance(request, HttpRequest)
    return render(request, 'app/yelp.html')
...

yelp.py

# -*- coding: utf-8 -*-

from __future__ import print_function

import json
import requests
import sys
import sqlite3
from urllib.parse import quote
from django.views.decorators.csrf import csrf_exempt
from django.shortcuts import render, redirect
from app.models import Business
from django.shortcuts import get_object_or_404

API_KEY = '*****************************************************************************'

# API constants, you shouldn't have to change these.
API_HOST = 'https://api.yelp.com'
SEARCH_PATH = '/v3/businesses/search'
BUSINESS_PATH = '/v3/businesses/'  # Business ID will come after slash.

# Defaults
DEFAULT_TERM = 'dinner'
DEFAULT_LOCATION = 'Glassboro, NJ'
SEARCH_LIMIT = 3
OFFSET = 0

def request(host, path, api_key, url_params=None):
    url_params = url_params or {}
    url = '{0}{1}'.format(host, quote(path.encode('utf8')))
    headers = {
        'Authorization': 'Bearer %s' % api_key,
    }

    print(u'Querying {0} ...'.format(url))

    response = requests.request('GET', url, headers=headers, params=url_params)

    return response.json()

def search(api_key, term, location):
    url_params = {
        'term': term.replace(' ', '+'),
        'location': location.replace(' ', '+'),
        'limit': SEARCH_LIMIT,
        'offset': OFFSET
    }
    return request(API_HOST, SEARCH_PATH, api_key, url_params=url_params)

def get_business(api_key, business_id):
    business_path = BUSINESS_PATH + business_id

    return request(API_HOST, business_path, api_key)

def query_api(request, term, location):
    
    print("query_api", term, location)
    response = search(API_KEY, term, location)

    businesses = response.get('businesses')

    if not businesses:
        print(u'No businesses for {0} in {1} found.'.format(term, location))
        return

    business_id = businesses[0]['id']

    response = get_business(API_KEY, business_id)

    write_to_file = json.dumps([response], indent=4)

    with open('app/API/YelpAPI/yelp.json', 'w') as f:
        f.write(write_to_file)
    
    yelp_data = Business.objects.filter(business_id=business_id).order_by('-id').first()
    dic = {
        'yelp_data': yelp_data,
        }
    print(dic)
    return render(request, 'app/yelp.html', dic)

def db():
    
    with open('app/API/YelpAPI/yelp.json', 'r') as f:
        data = json.load(f)

    conn = sqlite3.connect('capstone/db.sqlite3')
    cur = conn.cursor()
        # Create the table if it doesn't exist.
    cur.execute(
        """CREATE TABLE IF NOT EXISTS Business(
                id INTEGER PRIMARY KEY,
                business_id varchar(100),
                business_name varchar(100),
                yelp_business_id varchar(254),
                phone varchar(15),
                city varchar(100),
                state varchar(20),
                address varchar(100),
                postal_code varchar(15),
                latitude float(100),
                longitude float(100),
                business_stars float(10),
                business_review_count integer(10),
                is_open integer(1)
            );"""
    )
    
    business_id = data[0]['id']
    business_name = data[0]['name']
    yelp_business_id = data[0]['alias']
    phone = data[0]['phone']
    city = data[0]['location']['city']
    state = data[0]['location']['state']
    address = data[0]['location']['address1']
    postal_code = data[0]['location']['zip_code']
    latitude = data[0]['coordinates']['latitude']
    longitude = data[0]['coordinates']['longitude']
    business_stars = data[0]['rating']
    business_review_count = data[0]['review_count']
    is_open = data[0]['hours'][0]['is_open_now']
        
    # Execute the command and replace '?' with the each value
    # in 'values'. DO NOT build a string and replace manually.
    # the sqlite3 library will handle non safe strings by doing this.
    sql = '''INSERT INTO Business (business_id, business_name, yelp_business_id, phone, city,  state, address, postal_code, latitude, longitude, business_stars, business_review_count, is_open) VALUES(
                ?,
                ?,
                ?,
                ?,
                ?,
                ?,
                ?,
                ?,
                ?,
                ?,
                ?,
                ?,
                ?
            );'''
    cur.execute(sql, (business_id, business_name, yelp_business_id, phone, city, state, address, postal_code, latitude, longitude, business_stars, business_review_count, is_open, ))

    conn.commit()
    conn.close()

@csrf_exempt
def yelp_main(request):
    #request.session._get_or_create_session_key()
    term = request.POST.get('term')
    location = request.POST.get('location')
    db()
    print("yelp_api", term, location)
    query_api(request, term, location)
    return render(request, 'app/yelp.html')

yelp.html

{% extends "app/layout.html" %}

{% block content %}

<h2>{{ title }}</h2>
<form name='YelpForm' action="" class="post-form" method="post">
    {% csrf_token %}
    <label for="term">Term: </label>
    <input id="term" type="text" name="term" placeholder="Type..." value="{{ YelpForm.term }}">
    <label for="location">Location: </label>
    <input id="location" type="text" name="location" placeholder="Where..." value="{{ YelpForm.location }}">
    <input type="submit" value="OK">
</form>
<h1>{{ yelp_data.business_name }}</h1>
{% endblock %}

app/urls.py

from django.urls import re_path, path
from app import views
from .API.YelpAPI.yelp import yelp_main, query_api

# SET THE NAMESPACE!
app_name = 'app'
# Be careful setting the name to just /login use userlogin instead!
urlpatterns=[
    path('yelp_main/', yelp_main, name='yelp_main' ),
    path('query_api/', query_api, name='query_api'),
    path('registration/', views.register_request, name='registration'),
    re_path(r'^user_login/$', views.user_login,name='user_login'),
]

capstone/urls.py

from datetime import datetime
from django.urls import re_path, path, include
from django.contrib import admin
from django.contrib.auth.views import LoginView, LogoutView
from app import forms, views

urlpatterns = [
    path('home/', views.home, name='home'),
    path('contact/', views.contact, name='contact'),
    path('about/', views.about, name='about'),
    path('admin/', admin.site.urls),
    re_path(r'^$',views.index,name='index'),
    re_path(r'^special/',views.special,name='special'),
    re_path('', include('app.urls', namespace='app')),
    re_path(r'^logout/$', views.user_logout, name='logout'),
    path('foodie/', views.foodie, name='foodie'),
    path('yelping/', views.yelping, name='yelping'),
]

models.py

...
class Business(models.Model):
    business_id = models.CharField(max_length=100)
    business_name = models.CharField(max_length=100)
    yelp_business_id = models.CharField(max_length=45, blank=True, null=True)
    phone = models.CharField(max_length=100)
    city = models.CharField(max_length=100)
    state = models.CharField(max_length=100)
    address = models.CharField(max_length=100, blank=True, null=True)
    postal_code = models.CharField(max_length=15, blank=True, null=True)
    latitude = models.DecimalField(max_digits=100, decimal_places=2, blank=True, null=True)
    longitude = models.DecimalField(max_digits=100, decimal_places=2, blank=True, null=True)
    business_stars = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=False)
    business_review_count = models.IntegerField(blank=True, null=True)
    is_open = models.IntegerField(blank=True, null=True)

    class Meta:
        managed = True
        db_table = 'business'
        ordering = ['business_name', 'business_stars', 'is_open']
        verbose_name = 'Business Information'
        verbose_name_plural = "Business's Information"

    def __str__(self):
        return self.business_name
...

class YelpInputModel(models.Model):
    term = models.CharField(blank=True, null=False, max_length=100)
    location = models.CharField(blank=True, null=False, max_length=100)

    class Meta:
        managed = True
        db_table = 'yelpinputmodel'
...

python.exe

Performing system checks...

System check identified no issues (0 silenced).
November 11, 2022 - 10:20:42
Django version 4.1.3, using settings 'capstone.settings.development'
Starting development server at http://127.0.0.1:61098/
Quit the server with CTRL-BREAK.
[11/Nov/2022 10:20:43] "GET / HTTP/1.1" 200 2624
[11/Nov/2022 10:20:44] "GET /static/app/content/site.css HTTP/1.1" 200 723
[11/Nov/2022 10:20:44] "GET /static/app/content/bootstrap.min.css HTTP/1.1" 200 97968
[11/Nov/2022 10:20:44] "GET /static/app/scripts/modernizr-2.6.2.js HTTP/1.1" 200 52810
[11/Nov/2022 10:20:44] "GET /favicon.ico HTTP/1.1" 404 3796
[11/Nov/2022 10:20:50] "GET /yelping HTTP/1.1" 301 0
[11/Nov/2022 10:20:50] "GET /yelping/ HTTP/1.1" 200 2035
yelping dinner 08501
yelp_api dinner 08501
query_api dinner 08501
Querying https://api.yelp.com/v3/businesses/search ...
Querying https://api.yelp.com/v3/businesses/a0FEhgrOIk_M6_o0ie-NpQ ...
{'yelp_data': <Business: The Roost Restaurant>}
[11/Nov/2022 10:20:54] "POST /yelping/ HTTP/1.1" 200 2095
Asked By: Diggty66

||

Answers:

Problem

I think the bottom line is that your first instinct that the page yelp.html was being overwritten was correct. Your yelping returns render(request, 'app/yelp.html'), which has no data in it, because no context has been given to it. Now this view function first calls yelp_main(request), which also returns render(request, 'app/yelp.html'), the page with no data again. But it does call def query_api(request, term, location):. This does return data with the page since it has a context, dic with it, render(request, 'app/yelp.html', dic). But query_api returns to yelp_main which overwrites it with the no data version of the page, and then the original calling function replaces that with another no data version of the page.

Solution

Instead of returning pages with render, could you just return an empty HttpResponse, and then only at the end send the dictionary:

# views.py

from django.http import HttpResponse

@csrf_exempt
def yelping(request):

    form = YelpForm(request.POST or None)
    
    if form.is_valid():
        form.save(commit=False)
        term = request.POST['term']
        location = request.POST['location']
        form.save()
        print("yelping", term, location)
        yelp_main(request)
        messages.success(request, "Search successful." )
        return redirect('app:yelping')
        #return render(request, 'app/yelp.html', {'form' : form})
        
    messages.error(request, "Unsuccessful Search. Invalid information.")
        
    assert isinstance(request, HttpRequest)

    # NOTE: I removed the .first() from the following line
    yelp_data = Business.objects.filter(business_id=business_id).order_by('-id')

    dic = {
        'yelp_data': yelp_data,
        }
    print(dic)
    
    return render(request, 'app/yelp.html', dic)

and

# yelp.py

from django.http import HttpResponse

@csrf_exempt
def yelp_main(request):
    #request.session._get_or_create_session_key()
    term = request.POST.get('term')
    location = request.POST.get('location')
    db()
    print("yelp_api", term, location)
    query_api(request, term, location)
    return HttpResponse("OK")
    #return render(request, 'app/yelp.html')


def query_api(request, term, location):
    
    print("query_api", term, location)
    response = search(API_KEY, term, location)

    businesses = response.get('businesses')

    if not businesses:
        print(u'No businesses for {0} in {1} found.'.format(term, location))
        return

    business_id = businesses[0]['id']

    response = get_business(API_KEY, business_id)

    write_to_file = json.dumps([response], indent=4)

    with open('app/API/YelpAPI/yelp.json', 'w') as f:
        f.write(write_to_file)
    
    return HttpResponse("OK")
    #return render(request, 'app/yelp.html', dic)

Edit

Why the dict is only printing one piece of data?
I may have misunderstood by what you meant by one piece of data.
Business.objects.filter(business_id=business_id).order_by('-id').first() will return the Business object, which includes all the fields of that class, but if you print it it will only show the business_name. Why? Because that’s what you asked Django to print when you defined

def __str__(self):
    return self.business_name

If, as I think you want to print out all the rows, then remove the .first() since that will return one Business object, which is obviously not something you can iterate through, giving you "Business is not iterable". Then in your HTML you should be able to do:

{% for y in yelp_data %}
    <td>{{ y.business_name }}</td>
    <td>{{ y.phone }}</td>
    <td>{{ y.city }}</td>
    ...
{% endfor %}
Answered By: raphael
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.