Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Added Subregion class and replaced region_# naming scheme.

Moved common code from Region/City/District into base classes.
  • Loading branch information...
commit 85f2f700a286b4a3a12e41fe7c65deafe87c20e4 1 parent 8c1f51c
@Qarterd Qarterd authored
View
20 README.md
@@ -40,6 +40,13 @@ Nearest city to a given geo-coord (longitude, latitude):
nearest = City.objects.distance(london.location).exclude(id=london.id).order_by('distance')[:5]
```
+Get a list of all cities in a state or county:
+
+```python
+ City.objects.filter(country__name="United States", region__name="Texas")
+ City.objects.filter(country__name="United States", subregion__name="Orange County")
+```
+
Get all countries in Japanese preferring official names if available, fallback on ASCII names:
```python
@@ -58,23 +65,20 @@ Gather names of Tokyo from all CITIES_LOCALES:
[name for locale in cities.conf.settings.locales
for name in geo_alt_names[City][locale].objects.filter(geo__name='Tokyo')]
```
+
Get all postal codes for Ontario, Canada (only first 3 letters available due to copyright restrictions):
```python
- postal_codes['CA'].objects.filter(region_0_name='Ontario')
+ postal_codes['CA'].objects.filter(region__name='Ontario')
```
Get region objects for US postal code:
```python
Region.objects.filter(postal_codes_US__code='90210')
- [<Region: Los Angeles County, California, United States>]
-```
-
-Get a list of cities in the state of Texas:
-
-```python
- City.objects.filter(country__name="United States", region__region_parent__name="Texas")
+ [<Region: California, United States>]
+ Subregion.objects.filter(postal_codes_US__code='90210')
+ [<Subregion: Los Angeles County, California, United States>]
```
Install
View
29 cities/admin.py
@@ -7,26 +7,31 @@ class CountryAdmin(admin.ModelAdmin):
admin.site.register(Country, CountryAdmin)
-class RegionAdmin(admin.ModelAdmin):
+class RegionBaseAdmin(admin.ModelAdmin):
ordering = ['name_std']
- list_display = ['name_std', 'parent', 'code', 'level']
+ list_display = ['name_std', 'parent', 'code']
search_fields = ['name', 'name_std', 'code']
- raw_id_fields = ['region_parent']
+class RegionAdmin(RegionBaseAdmin): pass
+
admin.site.register(Region, RegionAdmin)
-class CityAdmin(admin.ModelAdmin):
- ordering = ['name_std']
- list_display = ['name_std', 'parent', 'population']
- search_fields = ['name', 'name_std']
+class SubregionAdmin(RegionBaseAdmin):
raw_id_fields = ['region']
-admin.site.register(City, CityAdmin)
+admin.site.register(Subregion, SubregionAdmin)
-class DistrictAdmin(admin.ModelAdmin):
+class CityBaseAdmin(admin.ModelAdmin):
ordering = ['name_std']
list_display = ['name_std', 'parent', 'population']
search_fields = ['name', 'name_std']
+
+class CityAdmin(CityBaseAdmin):
+ raw_id_fields = Region.levels
+
+admin.site.register(City, CityAdmin)
+
+class DistrictAdmin(CityBaseAdmin):
raw_id_fields = ['city']
admin.site.register(District, DistrictAdmin)
@@ -42,8 +47,8 @@ class GeoAltNameAdmin(admin.ModelAdmin):
class PostalCodeAdmin(admin.ModelAdmin):
ordering = ['code']
- list_display = ['code', 'name', 'region_0_name', 'region_1_name', 'region_2_name']
- search_fields = ['code', 'name', 'region_0_name', 'region_1_name', 'region_2_name']
- raw_id_fields = ['region']
+ list_display = ['code', 'name', 'region_name', 'subregion_name', 'district_name']
+ search_fields = ['code', 'name', 'region_name', 'subregion_name', 'district_name']
+ raw_id_fields = Region.levels
[admin.site.register(postal_code, PostalCodeAdmin) for postal_code in postal_codes.values()]
View
9 cities/conf.py
@@ -19,11 +19,11 @@
'filename': 'countryInfo.txt',
'urls': [url_bases['geonames']['dump']+'{filename}', ]
},
- 'region_0': {
+ 'region': {
'filename': 'admin1CodesASCII.txt',
'urls': [url_bases['geonames']['dump']+'{filename}', ]
},
- 'region_1': {
+ 'subregion': {
'filename': 'admin2Codes.txt',
'urls': [url_bases['geonames']['dump']+'{filename}', ]
},
@@ -70,8 +70,7 @@
'all',
'country',
'region',
- 'region_0',
- 'region_1',
+ 'subregion',
'city',
'district',
'alt_name',
@@ -81,6 +80,7 @@
import_opts_all = [
'country',
'region',
+ 'subregion',
'city',
'district',
'alt_name',
@@ -94,6 +94,7 @@ class HookException(Exception): pass
plugin_hooks = [
'country_pre', 'country_post',
'region_pre', 'region_post',
+ 'subregion_pre', 'subregion_post',
'city_pre', 'city_post',
'district_pre', 'district_post',
'alt_name_pre', 'alt_name_post',
View
166 cities/management/commands/cities.py
@@ -4,7 +4,8 @@
http://download.geonames.org/export/dump/
- Countries: countryInfo.txt
-- Regions: admin1CodesASCII.txt, admin2Codes.txt
+- Regions: admin1CodesASCII.txt
+- Subregions: admin2Codes.txt
- Cities: cities5000.zip
- Districts: hierarchy.zip
- Localization: alternateNames.zip
@@ -20,6 +21,7 @@
import zipfile
import time
from collections import namedtuple, defaultdict
+from itertools import chain
from optparse import make_option
from django.core.management.base import BaseCommand
from django.utils.encoding import force_unicode
@@ -138,15 +140,19 @@ def get_data(self, filekey):
file.close()
return data
+ def parse(self, data):
+ for line in data:
+ if len(line) < 1 or line[0] == '#': continue
+ items = [e.strip() for e in line.split('\t')]
+ yield items
+
def import_country(self):
uptodate = self.download('country')
if uptodate and not self.force: return
data = self.get_data('country')
self.logger.info("Importing country data")
- for line in data:
- if len(line) < 1 or line[0] == '#': continue
- items = [e.strip() for e in line.split('\t')]
+ for items in self.parse(data):
if not self.call_hook('country_pre', items): continue
country = Country()
@@ -162,34 +168,21 @@ def import_country(self):
if not self.call_hook('country_post', country, items): continue
country.save()
self.logger.debug("Added country: {}, {}".format(country.code, country))
-
- def import_region(self):
- self.import_region_0()
- self.import_region_1()
- def import_region_common(self, region, level, items):
+ def import_region_common(self, region, items):
+ class_ = region.__class__
region.id = int(items[3])
region.name = items[2]
region.name_std = items[1]
region.slug = slugify(region.name)
region.code = items[0]
- region.level = level
# Find country
country_code = region.code.split('.')[0]
try: region.country = self.country_index[country_code]
except:
- self.logger.warning("Region {}: {}: Cannot find country: {} -- skipping".format(level, region.name, country_code))
+ self.logger.warning("{}: {}: Cannot find country: {} -- skipping".format(class_.__name__, region.name, country_code))
return None
-
- # Find parent region, search highest level first
- for sublevel in reversed(range(level)):
- region_parent_code = '.'.join(region.code.split('.')[:sublevel+2])
- try:
- region.region_parent = self.region_index[region_parent_code]
- break
- except:
- self.logger.warning("Region {}: {}: Cannot find region {}: {}".format(level, region.name, sublevel, region_parent_code))
return region
@@ -201,61 +194,60 @@ def build_country_index(self):
for obj in Country.objects.all():
self.country_index[obj.code] = obj
- def import_region_0(self):
- uptodate = self.download('region_0')
+ def import_region(self):
+ uptodate = self.download('region')
if uptodate and not self.force: return
- data = self.get_data('region_0')
+ data = self.get_data('region')
self.build_country_index()
- self.logger.info("Importing region 0 data")
- for line in data:
- if len(line) < 1 or line[0] == '#': continue
- items = [e.strip() for e in line.split('\t')]
- if not self.call_hook('region_pre', 0, items): continue
+ self.logger.info("Importing region data")
+ for items in self.parse(data):
+ if not self.call_hook('region_pre', items): continue
- region = self.import_region_common(Region(), 0, items)
+ region = self.import_region_common(Region(), items)
if not region: continue
- if not self.call_hook('region_post', 0, region, items): continue
+ if not self.call_hook('region_post', region, items): continue
region.save()
- self.logger.debug("Added region 0: {}, {}".format(region.code, region))
+ self.logger.debug("Added region: {}, {}".format(region.code, region))
- def build_region_index(self, level=None):
- if hasattr(self, 'region_index') and self.region_index_level == level: return
+ def build_region_index(self):
+ if hasattr(self, 'region_index'): return
- if level is None:
- self.logger.info("Building region index")
- qset = Region.objects.all()
- else:
- self.logger.info("Building region {} index".format(level))
- qset = Region.objects.filter(level=level)
-
+ self.logger.info("Building region index")
self.region_index = {}
- for obj in qset:
+ for obj in chain(Region.objects.all(), Subregion.objects.all()):
self.region_index[obj.code] = obj
- self.region_index_level = level
- def import_region_1(self):
- uptodate = self.download('region_1')
+ def import_subregion(self):
+ uptodate = self.download('subregion')
if uptodate and not self.force: return
- data = self.get_data('region_1')
+ data = self.get_data('subregion')
self.build_country_index()
- self.build_region_index(0)
+ self.build_region_index()
- self.logger.info("Importing region 1 data")
- for line in data:
- if len(line) < 1 or line[0] == '#': continue
- items = [e.strip() for e in line.split('\t')]
- if not self.call_hook('region_pre', 1, items): continue
+ self.logger.info("Importing subregion data")
+ for items in self.parse(data):
+ if not self.call_hook('subregion_pre', items): continue
- region = self.import_region_common(Region(), 1, items)
- if not region: continue
+ subregion = self.import_region_common(Subregion(), items)
+ if not subregion: continue
- if not self.call_hook('region_post', 1, region, items): continue
- region.save()
- self.logger.debug("Added region 1: {}, {}".format(region.code, region))
+ # Find region
+ level = Region.levels.index("subregion") - 1
+ region_code = '.'.join(subregion.code.split('.')[:level+2])
+ try: subregion.region = self.region_index[region_code]
+ except:
+ self.logger.warning("Subregion: {}: Cannot find region: {}".format(subregion.name, region_code))
+ continue
+
+ if not self.call_hook('subregion_post', subregion, items): continue
+ subregion.save()
+ self.logger.debug("Added subregion: {}, {}".format(subregion.code, subregion))
+
+ del self.region_index
def import_city_common(self, city, items):
class_ = city.__class__
@@ -276,18 +268,18 @@ def import_city_common(self, city, items):
if class_ is City: city.country = country
# Find region, search highest level first
- region = None
item_offset = 10
- for level in reversed(range(2)):
+ for level, level_name in reversed(list(enumerate(Region.levels))):
if not items[item_offset+level]: continue
try:
code = '.'.join([country_code] + [items[item_offset+i] for i in range(level+1)])
region = self.region_index[code]
- break
+ if class_ is City:
+ setattr(city, level_name, region)
except:
- self.logger.log(logging.DEBUG if level else logging.WARNING, # Escalate if all levels failed
- "{}: {}: Cannot find region {}: {}".format(class_.__name__, city.name, level, code))
- if class_ is City: city.region = region
+ self.logger.log(logging.DEBUG if level else logging.WARNING, # Escalate if level 0 failed
+ "{}: {}: Cannot find {}: {}".format(class_.__name__, city.name, level_name, code))
+
return city
@@ -300,9 +292,7 @@ def import_city(self):
self.build_region_index()
self.logger.info("Importing city data")
- for line in data:
- if len(line) < 1 or line[0] == '#': continue
- items = [e.strip() for e in line.split('\t')]
+ for items in self.parse(data):
if not self.call_hook('city_pre', items): continue
type = items[7]
@@ -323,9 +313,7 @@ def build_hierarchy(self):
self.logger.info("Building hierarchy index")
self.hierarchy = {}
- for line in data:
- if len(line) < 1 or line[0] == '#': continue
- items = [e.strip() for e in line.split('\t')]
+ for items in self.parse(data):
parent_id = int(items[0])
child_id = int(items[1])
self.hierarchy[child_id] = parent_id
@@ -345,9 +333,7 @@ def import_district(self):
city_index[obj.id] = obj
self.logger.info("Importing district data")
- for line in data:
- if len(line) < 1 or line[0] == '#': continue
- items = [e.strip() for e in line.split('\t')]
+ for items in self.parse(data):
if not self.call_hook('district_pre', items): continue
type = items[7]
@@ -399,9 +385,7 @@ def import_alt_name(self):
}
self.logger.info("Importing alternate name data")
- for line in data:
- if len(line) < 1 or line[0] == '#': continue
- items = [e.strip() for e in line.split('\t')]
+ for items in self.parse(data):
if not self.call_hook('alt_name_pre', items): continue
# Only get names for languages in use
@@ -435,9 +419,7 @@ def import_postal_code(self):
self.build_region_index()
self.logger.info("Importing postal codes")
- for line in data:
- if len(line) < 1 or line[0] == '#': continue
- items = [e.strip() for e in line.split('\t')]
+ for items in self.parse(data):
if not self.call_hook('postal_code_pre', items): continue
country_code = items[0]
@@ -456,9 +438,9 @@ def import_postal_code(self):
pc.country = country
pc.code = code
pc.name = items[2]
- pc.region_0_name = items[3]
- pc.region_1_name = items[5]
- pc.region_2_name = items[7]
+ pc.region_name = items[3]
+ pc.subregion_name = items[5]
+ pc.district_name = items[7]
try: pc.location = Point(float(items[10]), float(items[9]))
except:
@@ -466,18 +448,16 @@ def import_postal_code(self):
pc.location = Point(0,0)
# Find region, search highest level first
- region = None
item_offset = 4
- for level in reversed(range(2)):
+ for level, level_name in reversed(list(enumerate(Region.levels))):
if not items[item_offset+level*2]: continue
try:
code = '.'.join([country_code] + [items[item_offset+i*2] for i in range(level+1)])
region = self.region_index[code]
- break
+ setattr(pc, level_name, region)
except:
- self.logger.log(logging.DEBUG if level else logging.WARNING, # Escalate if all levels failed
- "Postal code: {}, {}: Cannot find region {}: {}".format(pc.country, pc.code, level, code))
- pc.region = region
+ self.logger.log(logging.DEBUG if level else logging.WARNING, # Escalate if level 0 failed
+ "Postal code: {}, {}: Cannot find {}: {}".format(pc.country, pc.code, level_name, code))
if not self.call_hook('postal_code_post', pc, items): continue
pc.save()
@@ -488,16 +468,12 @@ def flush_country(self):
Country.objects.all().delete()
def flush_region(self):
- self.flush_region_0()
- self.flush_region_1()
-
- def flush_region_0(self):
- self.logger.info("Flushing region 0 data")
- Region.objects.filter(level=0).delete()
+ self.logger.info("Flushing region data")
+ Region.objects.all().delete()
- def flush_region_1(self):
- self.logger.info("Flushing region 1 data")
- Region.objects.filter(level=1).delete()
+ def flush_subregion(self):
+ self.logger.info("Flushing subregion data")
+ Subregion.objects.all().delete()
def flush_city(self):
self.logger.info("Flushing city data")
View
125 cities/models.py
@@ -3,8 +3,11 @@
from django.contrib.gis.geos import Point
from conf import settings
from util import create_model, un_camel
-
-__all__ = ['Point','Country','Region','City','District','geo_alt_names','postal_codes']
+
+__all__ = [
+ 'Point', 'Country', 'Region', 'Subregion',
+ 'City', 'District', 'geo_alt_names', 'postal_codes'
+]
class Place(models.Model):
name = models.CharField(max_length=200, db_index=True, verbose_name="ascii name")
@@ -12,6 +15,9 @@ class Place(models.Model):
objects = models.GeoManager()
+ class Meta:
+ abstract = True
+
@property
def hierarchy(self):
"""Get hierarchy, root first"""
@@ -19,18 +25,15 @@ def hierarchy(self):
list.append(self)
return list
- class Meta:
- abstract = True
-
def get_absolute_url(self):
return "/".join([place.slug for place in self.hierarchy])
-
+
class Country(Place):
code = models.CharField(max_length=2, db_index=True)
population = models.IntegerField()
continent = models.CharField(max_length=2)
tld = models.CharField(max_length=5)
-
+
class Meta:
ordering = ['name']
verbose_name_plural = "countries"
@@ -38,56 +41,69 @@ class Meta:
@property
def parent(self):
return None
-
+
def __unicode__(self):
return force_unicode(self.name)
-class Region(Place):
+class RegionBase(Place):
name_std = models.CharField(max_length=200, db_index=True, verbose_name="standard name")
code = models.CharField(max_length=200, db_index=True)
- level = models.IntegerField(db_index=True, verbose_name="admin level") # Level 0 has no parent region
- region_parent = models.ForeignKey('self', null=True, blank=True, related_name='region_children')
country = models.ForeignKey(Country)
+ levels = ['region', 'subregion']
+
+ class Meta:
+ abstract = True
+
+ def __unicode__(self):
+ return u'{}, {}'.format(force_unicode(self.name_std), self.parent)
+
+class Region(RegionBase):
+
@property
def parent(self):
- """Returns parent region if available, otherwise country"""
- return self.region_parent or self.country
-
+ return self.country
+
+class Subregion(RegionBase):
+ region = models.ForeignKey(Region)
+
+ @property
+ def parent(self):
+ return self.region
+
+class CityBase(Place):
+ name_std = models.CharField(max_length=200, db_index=True, verbose_name="standard name")
+ location = models.PointField()
+ population = models.IntegerField()
+
+ class Meta:
+ abstract = True
+
def __unicode__(self):
return u'{}, {}'.format(force_unicode(self.name_std), self.parent)
-
-class City(Place):
- name_std = models.CharField(max_length=200, db_index=True, verbose_name="standard name")
+
+class City(CityBase):
region = models.ForeignKey(Region, null=True, blank=True)
+ subregion = models.ForeignKey(Subregion, null=True, blank=True)
country = models.ForeignKey(Country)
- location = models.PointField()
- population = models.IntegerField()
-
+
class Meta:
verbose_name_plural = "cities"
-
+
@property
def parent(self):
- """Returns region if available, otherwise country"""
- return self.region if self.region else self.country
-
- def __unicode__(self):
- return u'{}, {}'.format(force_unicode(self.name_std), self.parent)
+ for parent_name in reversed(['country'] + RegionBase.levels):
+ parent_obj = getattr(self, parent_name)
+ if parent_obj: return parent_obj
+ return None
-class District(Place):
- name_std = models.CharField(max_length=200, db_index=True, verbose_name="standard name")
+class District(CityBase):
city = models.ForeignKey(City)
- location = models.PointField()
- population = models.IntegerField()
-
+
@property
def parent(self):
return self.city
- def __unicode__(self):
- return u'{}, {}'.format(force_unicode(self.name_std), self.parent)
-
class GeoAltNameManager(models.GeoManager):
def get_preferred(self, default=None, **kwargs):
"""
@@ -98,7 +114,7 @@ def get_preferred(self, default=None, **kwargs):
except self.model.DoesNotExist:
try: return self.filter(**kwargs)[0]
except IndexError: return default
-
+
def create_geo_alt_names(geo_type):
geo_alt_names = {}
for locale in settings.locales:
@@ -108,7 +124,7 @@ def create_geo_alt_names(geo_type):
name = name,
fields = {
'geo': models.ForeignKey(geo_type, # Related geo type
- related_name = 'alt_names_' + locale),
+ related_name = 'alt_names_' + locale),
'name': models.CharField(max_length=200, db_index=True), # Alternate name
'is_preferred': models.BooleanField(), # True if this alternate name is an official / preferred name
'is_short': models.BooleanField(), # True if this is a short name like 'California' for 'State of California'
@@ -126,38 +142,42 @@ def create_geo_alt_names(geo_type):
return geo_alt_names
geo_alt_names = {}
-for type in [Country, Region, City, District]:
+for type in [Country, Region, Subregion, City, District]:
geo_alt_names[type] = create_geo_alt_names(type)
+
def create_postal_codes():
+
@property
def parent(self):
- """Returns region if available, otherwise country"""
- return self.region if self.region else self.country
-
+ for parent_name in reversed(['country'] + RegionBase.levels):
+ parent_obj = getattr(self, parent_name)
+ if parent_obj: return parent_obj
+ return None
+
@property
def hierarchy(self):
"""Get hierarchy, root first"""
list = self.parent.hierarchy
list.append(self)
return list
-
+
@property
def names(self):
"""Get a hierarchy of non-null names, root first"""
return [e for e in [
force_unicode(self.country),
- force_unicode(self.region_0_name),
- force_unicode(self.region_1_name),
- force_unicode(self.region_2_name),
+ force_unicode(self.region_name),
+ force_unicode(self.subregion_name),
+ force_unicode(self.district_name),
force_unicode(self.name),
] if e]
-
+
@property
def name_full(self):
"""Get full name including hierarchy"""
return u', '.join(reversed(self.names))
-
+
postal_codes = {}
for country in settings.postal_codes:
name_format = "{}" + country
@@ -169,11 +189,12 @@ def name_full(self):
related_name = 'postal_codes_' + country),
'code': models.CharField(max_length=20, primary_key=True),
'name': models.CharField(max_length=200, db_index=True),
- 'region_0_name': models.CharField(max_length=100, db_index=True, verbose_name="region 0 name (state)"),
- 'region_1_name': models.CharField(max_length=100, db_index=True, verbose_name="region 1 name (county)"),
- 'region_2_name': models.CharField(max_length=100, db_index=True, verbose_name="region 2 name (community)"),
- 'region': models.ForeignKey(Region, null=True, blank=True,
- related_name = 'postal_codes_' + country),
+ # Region names for each admin level, region may not exist in DB
+ 'region_name': models.CharField(max_length=100, db_index=True),
+ 'subregion_name': models.CharField(max_length=100, db_index=True),
+ 'district_name': models.CharField(max_length=100, db_index=True),
+ 'region': models.ForeignKey(Region, null=True, blank=True, related_name = 'postal_codes_' + country),
+ 'subregion': models.ForeignKey(Subregion, null=True, blank=True, related_name = 'postal_codes_' + country),
'location': models.PointField(),
'objects': models.GeoManager(),
'parent': parent,
@@ -193,3 +214,5 @@ def name_full(self):
return postal_codes
postal_codes = create_postal_codes()
+
+
View
12 setup.py
@@ -1,16 +1,20 @@
from setuptools import setup, find_packages
import os
+# Utility function to read the README file.
+# Used for the long_description. It's nice, because now 1) we have a top level
+# README file and 2) it's easier to type in the README file than to put a raw
+# string in below ...
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
setup(
name='django-cities',
- version='0.19',
+ version='0.2',
description='Place models and data for Django apps',
- author='Ben Dowling',
- author_email='ben.m.dowling@gmail.com',
- url='https://github.com/coderholic/django-cities',
+ author='Dan Carter (original by Ben Dowling)',
+ author_email='carterd@gmail.com',
+ url='https://github.com/Kometes/django-cities',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
Please sign in to comment.
Something went wrong with that request. Please try again.