How to get a list of cities in a country from database?

Viewed 772

I have 2 models, City and Country. I want to show cities in the country on a page based on the country selected. I tried to query the Country model passing it to the City model to find all cities related to the country but currently, pages show all cities no matter what is the country.

How I can show cities in one country when the user selects the country from the page?

Any help or suggestions are highly appreciated.

models.py

class City(models.Model):
    country = models.ForeignKey('Country', on_delete=models.CASCADE, related_name='country')
    name = models.CharField(max_length=90, verbose_name='City name')
    slug = models.SlugField(null=True, unique=True)

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('city:cities', kwargs={'slug': self.slug})

     def save(self, *args, **kwargs): 
         if not self.slug:
             self.slug = slugify(self.name)
         return super().save(*args, **kwargs)

class Country(models.Model):
    country_name = models.CharField(max_length=50, verbose_name='Country name',unique=True)
    slug = models.SlugField(null=True, unique=True)

    def __str__(self):
        return self.country_name

    def get_absolute_url(self):
       return reverse('city:cities', kwargs={'slug': self.slug})

    def save(self, *args, **kwargs): 
         if not self.slug:
             self.slug = slugify(self.country_name)
         return super().save(*args, **kwargs)

views.py

class CountriesListView(ListView):
    template_name = "countries.html"
    model = Country

    context_object_name = "countries"

class CitiesListView(ListView):
    template_name = "cities_list.html"
    model = City

    context_object_name = "cities"

    def get_context_data(self, *args, **kwargs):
        context = super(CitiesListView, self).get_context_data(*args,**kwargs)
        countries = Country.objects.all()
        cities = City.objects.filter(country__in=countries)
        context['cities'] = cities
        return context

templates

# countries.html

<h1>Countries</h1>

{% for country in countries %}
  <h3><a href="{% url 'city:cities' country.slug %}">{{ country.country_name }}</h3>
{% endfor %}


# cities_list.html

<h1>Cities</h1>

{% for city in cities %}
  <div>
        <h3><a href="{% url 'city:details' city.slug %}">{{ city }}</a></h3>
  </div>  
{% endfor %}
1 Answers

The related_name=… parameter [Django-doc] is the name of the relation in reverse, so to access the City objects of a specific Country. Therefore naming it Country is not a good idea. You can rename it to cities for example:

class City(models.Model):
    country = models.ForeignKey(
        'Country',
        on_delete=models.CASCADE,
        related_name='cities'
    )
    # …

You can use a DetailView to render a single Country, so:

from django.views.generic.detail import DetailView

class CountryDetailView(DetailView):
    template_name = 'country_detail.html'
    model = Country
    context_object_name = 'country'

In the urls, you can thus register this view under the name cities with a slug field:

# app/urls.py

from django.urls import path

from . import views

app_name='city'

urlpatterns = [
    # …,
    path(
        'country/<slug:slug>',
        views.CountryDetailView.as_view(),
        name='cities'
    ),
]

In the template, you can then access the relation in reverse:

<!-- country_detail.html -->

{{ country }}

{% for city in country.cities.all %}
    {{ city }}
{% endfor %}
Related