Еще одна реализация поля «город» для Django

«Еще одна» — потому что мне кажется, что я что-то упускаю и, в действительности, есть хорошее, но неизвестное мне решение “из коробки”. Тем не менее, вот мой рецепт:



Данные


В первую очередь, возник вопрос — где брать список городов. Исторически, я остановил свой выбор на geonames.org c creative commons лицензией. OpenStreetMaps от него отказались, из-за возможных “патентных” претензий от Google, где Geonames берет часть данных. (Тем не менее, на http://www.openstreetmap.org/ он используется как альтернативный источник информации.)

Учитывая поверхностность интересов (в данном случае был нужен просто город, как поле профайла, без геоопераций вроде поиска вхождений в область и т.д.), нужно преобразовать данные geonames в подходящий нам, простой формат.
Я не тратил время на поиск красивого решения и сделал все просто, с помощью SQL и дополнительной базы:

1. Создаем базу geonames и импортируем туда данные (http://forum.geonames.org/gforum/posts/list/732.page)
На этом можно остановиться и использовать их в таком виде, а можно преобразовать чтобы подчинить привязать к своей логике.
2. Преобразуем. Не претендую на единственно правильное решение, некоторые, вызывающие технические вопросы страны я порезал:

-- Country import
insert into common_country(id,name,code,population,latitude,longitude,alternatenames) select geonameid as id,name,country as code, population,latitude,longitude,alternatenames from  geonames.geoname gn  where (gn.fcode IN ('PCLI','PCLIX','PCLD','PCLS','PCLF','PCL','TERR'));

delete from common_country where id in (2077507,2170371,7910334,7909807);

create unique index common_country_idx on common_country (id);
create index common_country_code_idx on common_country(code);

-- South Korea Fix
update common_country set alternatenames = concat(alternatenames,"Korea, Republic of") where id = 1835841;

-- City import
insert into common_city(id,name,country_id,alternatenames,latitude,longitude, adm) select gn.geonameid as id, gn.name, c.id as country_id, gn.alternatenames, gn.latitude, gn.longitude, admin1 as adm from geonames.geoname gn left join common_country as c on gn.country=c.code where (gn.fcode in ('PPL','PPLC','PPLA','PPLA2','PPLA3','PPLA4'));


Очевидный минус — необходимость написания функционала для обновления данных. Найденные мной решения заточены под PostgreSQL.

Средства поиска



Родной full-text поиск MySQL не справлялся с быстрым поиском по такому количеству населенных пунктов, поэтому я использовал Sphinxsearch (думаю, подойдет любой другой Solr)
Конфиг индекса и источника:

source geo_city
{
    type                = mysql
    sql_host            = localhost
    sql_user            = citylist
    sql_pass            = citylist
    sql_db              = citylist
    sql_port            =

    sql_query_pre       = SET NAMES utf8
    sql_query_post      =
    sql_query           = \
        SELECT id, name, country_id, alternatenames, latitude, longitude\
        FROM geo_city
    sql_query_info      = SELECT * FROM `geo_city` WHERE `id` = $id

    # ForeignKey's
    sql_attr_uint       = country_id

}

index common_city
{
    source          = geo_city
    path            = /var/lib/sphinxsearch/data/geo_city
    docinfo         = extern
    morphology      = none
    stopwords       =
    min_word_len    = 2
    charset_type    = utf-8
    min_prefix_len  = 2
    min_infix_len   = 0
    prefix_fields   = name, alternatenames
    enable_star     = 1
}


Представление



Нам понадобятся:
django-selectable
django-sphinx
django-profiles – для моего конкретного случая с профайлом

Поставим оба приложения, добавим их в INSTALLED_APPS.
Django-sphinx требует номера версии API в settings.py

для Sphinx 0.9.9:
SPHINX_API_VERSION = 0x116


создадим модель для города и страны в приложении, отвечающем за гео логику/модели
(geo в моем случае)

class Country(models.Model):
    name = models.CharField(max_length=200)
    code = models.CharField(max_length=10)
    population = models.IntegerField()
    latitude = models.FloatField()
    longitude = models.FloatField()
    alternatenames = models.CharField(max_length=2000, blank=True, default='')

    def __str__(self):
            return unicode(self.name).encode('utf-8')

    def __unicode__(self):
            return unicode(self.name)


class City(models.Model):

    name = models.CharField(max_length=200)
    country = models.ForeignKey(Country)
    alternatenames = models.CharField(max_length=2000, blank=True, default='')
    latitude = models.FloatField(default=0)
    longitude = models.FloatField(default=0)
    adm = models.CharField(max_length=200)
    search = SphinxSearch(weights={
            'name': 100,
            'alternatenames': 80
        })

    def __str__(self):
            return unicode(self.name).encode('utf-8')

    def __unicode__(self):
            return unicode(self.name)


Если поле города нужно для профайла, добавляем его и модель в приложение, которое занимается профайлами, у меня это usermanage:

class CustomUserProfile(models.Model):
    user = models.ForeignKey(User, unique=True)
    full_name = models.CharField(max_length=200, blank=True)
    city = models.ForeignKey(City, blank=True, null=True)
    country = models.ForeignKey(Country, blank=True, null=True, editable=False)
    date_registered = models.DateField(editable=False, auto_now_add=True)


и форму профайла:

class UserProfileForm(forms.ModelForm):
    ''' Form to edit profile'''
    full_name = forms.CharField(widget=forms.TextInput())
    city = selectable.AutoCompleteSelectField(
        label='City please',
                lookup_class = common.lookups.CityLookup,
        required=False,
    )

    def clean_city(self):
        """
        Convert city code to city object
        
        """
        city_id = int(self.data["city_1"].encode("utf8"))
        city = City.objects.get(id=city_id)
        return city

    class Meta:
        model = CustomUserProfile
        exclude = ("user",)


и в settings.py

AUTH_PROFILE_MODULE = 'usermanage.CustomUserProfile'


подробнее о настройке django-profiles тут

django-selectable требует настройки urls.py

(r'^selectable/', include('selectable.urls')),


Создадим lookup.py и метод в нем для реализации запросов к базе и начального заполнения поля:

class CityLookup(LookupBase):

    model = City
    item = None

    def get_query(self, request, term):
        qs = self.model.search.query(term + "*")
        return qs

    def get_queryset(self):
        return None

    def get_item_id(self, item):
        return item.id

    def get_item_value(self, item):
        if (not self.item):
            return smart_unicode(item)
        return smart_unicode(self.item.name)

    def get_item_label(self, item):
        return u"%s, %s" % (item.name, item.country)

    def get_item(self, value):
        item = None
        if value:
            try:
                item = City.objects.get(id=value)
                self.item = item
            except IndexError:
                pass
        return item


try:
        registry.register(CityLookup)
except:
        pass


В forms.py профайла нужно импортировать lookups и поля/виджеты selectable

import selectable.forms as selectable
import geo.lookups</code>

Для работы django-selectable необходимы библиотеки jquery, добавляем их в base.html, предварительно скачав jquery и jquery-ui (jquery.dj.selectable.js — идет в django-selectable)
<script type="text/javascript" src="/js/jquery/jquery.min.js"></script>
<script type="text/javascript" src="/js/jquery/jquery-ui.min.js"></script>
<script type="text/javascript" src="/js/jquery/jquery.dj.selectable.js"></script>


Если все прошло успешно, то на выходе получится что-то такое:



или даже такое (поле alternatenames содержит варианты названий на разных языках)



Логичным продолжением, я вижу добавление в модель admin1 (административная единица первого уровня) для отделения населенных пунктов с одинаковым названием, тесты jemeter для замеров производительности и написание reusable application, хотя бы доработав django-geonames для MySQL, однако на последнем Kyiv.py я неожиданно осознал, что теперь путь проекта лежит в сторону geodjango, PostgreSQL и PostGIS, так как роль города в профайле становится больше, чем просто информационная.

P.S. Если кого-то интересует тестовый проект-демо, выложу.

Ссылки:
Список потенциальных источников данных OSM

github и bitbucket страницы описания соответствующих проектов.

Спасибо за внимание.
Share post

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 10

    +2
    Просто интересно. КАК КАК?! Как можно найти радикал? Он первый в поисковой выдаче? Или это добрая традиция — размещать картинки на хостинге, который для этого не предназначен.
    • UFO just landed and posted this here
        +1
        Каюсь, до вчерашней ночи я читал про стили изложения, про синтаксис, орфографию и необходимость ссылок. В свое оправдание могу сказать только что и видел и пользовался радикалом впервые — меня конечно навела на сомнения навящевая реклама, но все решила статья habrahabr.ru/blogs/hosting/89162/.Перезаливаю в срочном порядке.
          +1
          Забей на этих снобов.
          Статья отличная.
          Было бы супер если бы сделать названия таки в кирилице — режет глаз немного

          Для сфинкса какие настройки ставили?

          ЗЫ кстати варианты устойчивых хостов для картинок
          — яндекс картинки
          — гугл пикаса
          если не страшно что картинку изменят в размере, то
          — фейсбук
          — вконтакт
          • UFO just landed and posted this here
              +1
              Для сфинкса кроме того что в статье только сетевые и пути к логам. Названия можно сделать и в кириллице — у geonames есть таблица alternativenames с кодом языка названия, интересно было бы сделать поиск(выпадающий список) на том языке на котором вводят…
        +1
        Спасибо за ссылку на django-geonames, там и часовые пояса, и привязка к GeoDjango есть, оказывается :)

        В модели «город» еще обычно удобно иметь какие-то методы для работы с датами: текущее время, например.
          0
          Мы на проекте перепробовали множество решений для реализации гео-локаций, и скажу, что geonames пока-что действительно наиболее полная и лучшая база среди бесплатных аналогов. Особенно понравилось что страны, города и регионы переведены на множесво языков (очень большйо плюс для мультиязычных проектов) и много других плюшек.
            +1
            у меня автоматом выставляется из GeoIP. Если пользователь не согласен с тем как определил GeoIP то можно редактировать (на этот раз к полю ввода будет подцеплен автокомплит на любом набираемом языке code.google.com/p/geo-autocomplete/ )
              0
              огромное спасибо за статью!

              Only users with full accounts can post comments. Log in, please.