Skip to content

Merging First Featureset #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
venv
*.pyc
staticfiles
.env
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: gunicorn challenge.wsgi
Empty file added challenge/__init__.py
Empty file.
143 changes: 143 additions & 0 deletions challenge/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
"""
Django settings for challenge project on Heroku. For more info, see:
https://github.com/heroku/heroku-django-template

For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "b@-)68+qvva&l1vv7d4ox#42zul9p#+#h*js-8o!zwsuxmj9h#"

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

# Application definition

INSTALLED_APPS = [
'city',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
# Disable Django's own staticfiles handling in favour of WhiteNoise, for
# greater consistency between gunicorn and `./manage.py runserver`. See:
# http://whitenoise.evans.io/en/stable/django.html#using-whitenoise-in-development
'whitenoise.runserver_nostatic',
'django.contrib.staticfiles',
]

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'challenge.urls'

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
'debug': DEBUG,
},
},
]

WSGI_APPLICATION = 'challenge.wsgi.application'


# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases

AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]

# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/

LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True

DATABASES = {}
env = os.environ.copy()
db_url = env.get('DATABASE_URL', False)

if db_url:
import dj_database_url

# Update database configuration with $DATABASE_URL.
db_from_env = dj_database_url.config(conn_max_age=500)
DATABASES['default'].update(db_from_env)

else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}


# Honor the 'X-Forwarded-Proto' header for request.is_secure()
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

# Allow all host headers
ALLOWED_HOSTS = ['*']

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/

STATIC_ROOT = os.path.join(PROJECT_ROOT, 'staticfiles')
STATIC_URL = '/static/'

# Extra places for collectstatic to find static files.
STATICFILES_DIRS = [
os.path.join(PROJECT_ROOT, 'static'),
]

# Simplified static file serving.
# https://warehouse.python.org/project/whitenoise/
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
Empty file added challenge/static/humans.txt
Empty file.
26 changes: 26 additions & 0 deletions challenge/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""challenge URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.11/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf.urls import url
from django.contrib import admin
from city.views import suggestions
from city.models import set_max_min

urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^suggestions/', suggestions),
]

set_max_min()
18 changes: 18 additions & 0 deletions challenge/wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""
WSGI config for challenge project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application
from whitenoise.django import DjangoWhiteNoise

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "challenge.settings")

application = get_wsgi_application()
application = DjangoWhiteNoise(application)
Empty file added city/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions city/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib import admin

# Register your models here.
5 changes: 5 additions & 0 deletions city/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class CityConfig(AppConfig):
name = 'city'
27 changes: 27 additions & 0 deletions city/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-05-15 07:22
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='City',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.TextField()),
('latitude', models.FloatField()),
('longitude', models.FloatField()),
('population', models.IntegerField()),
('score', models.IntegerField()),
],
),
]
20 changes: 20 additions & 0 deletions city/migrations/0002_auto_20170515_0723.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-05-15 07:23
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('city', '0001_initial'),
]

operations = [
migrations.AlterField(
model_name='city',
name='score',
field=models.IntegerField(null=True),
),
]
Empty file added city/migrations/__init__.py
Empty file.
119 changes: 119 additions & 0 deletions city/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
from django.db import models
import csv
import sys
import math
import json
from difflib import SequenceMatcher

# Create your models here.
class ScoreCalculator:
def __init__(self, city, query):

self.score_map = {
"population": self.score_population,
"latitude": self.score_latitude,
"longitude": self.score_longitude,
"q": self.score_name
}

self.query = query
self.city = city

def calculate_score(self):
total_score = 0
for k in self.query.keys():
total_score += self.score_map[k]()

return total_score / len(self.query)

def score_population(self):
desired_pop = int(self.query["population"])
city_pop = self.city.population
return self.normalized_diff(desired_pop, city_pop, City.MAX_POP, City.MIN_POP)

def score_latitude(self):
desired_lat = float(self.query["latitude"])
city_lat = self.city.latitude
return self.normalized_diff(desired_lat, city_lat, City.MAX_LAT, City.MIN_LAT)

def score_longitude(self):
desired_lon = float(self.query["longitude"])
city_lon = self.city.longitude
return self.normalized_diff(desired_lon, city_lon, City.MAX_LON, City.MIN_LON)

def score_name(self):
desired_name = self.query["q"]
city_name = self.city.name
return SequenceMatcher(None, desired_name, city_name).ratio()

def normalized_diff(self, true, desired, maximum, minimum):
adjusted_true = self.normalize(true, maximum, minimum)
adjusted_desired = self.normalize(desired, maximum, minimum)
diff = abs(adjusted_true - adjusted_desired)
return 1 / (1 + diff)

def normalize(self, value, maximum, minimum):
return float(value - minimum) / float(maximum - minimum)

class City(models.Model):
MAX_POP = 0
MIN_POP = 0
MAX_LAT = 0
MIN_LAT = 0
MAX_LON = 0
MIN_LON = 0

name = models.TextField()
latitude = models.FloatField()
longitude = models.FloatField()
population = models.IntegerField()

def calculate_score(self, query):
self.score = ScoreCalculator(self, query.copy()).calculate_score()
return self.score

def dictionary(self):
return {
"name": self.name,
"latitude": self.latitude,
"longitude": self.longitude,
"population": self.population,
"score": "{0:.1f}".format(self.score)
}


def load_data():
csv.field_size_limit(sys.maxsize)
with open("./data/cities_canada-usa.tsv", "r") as f:
reader = csv.reader(f, delimiter="\t")
next(f)

cities = []
for row in reader:
cities.append(build_city(row))

City.objects.bulk_create(cities)

set_max_min()


def build_city(row):
_, _, name, _, lat, lon, _, _, _, _, _, _, _, _, pop, _, _, _, _ = row

city = City(
name=name,
latitude=float(lat),
longitude=float(lon),
population=int(pop),
)
return city


def set_max_min():
cities = City.objects.all()
City.MAX_LON = cities.latest("longitude").longitude
City.MIN_LON = cities.earliest("longitude").longitude
City.MAX_LAT = cities.latest("latitude").latitude
City.MIN_LAT = cities.earliest("latitude").latitude
City.MAX_POP = cities.latest("population").population
City.MIN_POP = cities.earliest("population").population
3 changes: 3 additions & 0 deletions city/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.test import TestCase

# Create your tests here.
Loading