Skip to content
This repository has been archived by the owner on May 24, 2019. It is now read-only.

Commit

Permalink
Compute country distance at aggregation time #280
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredlockhart committed May 2, 2016
1 parent 4e184fd commit 18ac116
Show file tree
Hide file tree
Showing 10 changed files with 89 additions and 75 deletions.
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ include =
omit =
leaderboard/wsgi
leaderboard/settings*
*migrations*

[report]
# Regexes for lines to exclude from consideration
Expand Down
39 changes: 39 additions & 0 deletions leaderboard/contributors/migrations/0012_auto_20160428_1827.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations
import django.contrib.gis.db.models.fields


def set_contribution_point_to_country_center(apps, schema_editor):
Contribution = apps.get_model('contributors', 'Contribution')
db_alias = schema_editor.connection.alias

for contribution in Contribution.objects.using(db_alias):
contribution.point = contribution.country.geometry.point_on_surface
contribution.save()


def noop(apps, schema_editor):
pass


class Migration(migrations.Migration):

dependencies = [
('locations', '0006_auto_20160208_2050'),
('contributors', '0011_auto_20160208_2034'),
]

operations = [
migrations.AddField(
model_name='contribution',
name='point',
field=django.contrib.gis.db.models.fields.PointField(srid=4326, null=True, blank=True),
),
migrations.RunPython(set_contribution_point_to_country_center, noop),
migrations.RemoveField(
model_name='contribution',
name='country',
),
]
14 changes: 11 additions & 3 deletions leaderboard/contributors/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import operator

from bulk_update.helper import bulk_update
from django.db import models
from django.conf import settings
from django.contrib.gis.db import models

from leaderboard.locations.models import Country

Expand Down Expand Up @@ -66,11 +67,18 @@ def _compute_ranks(cls, contributions):
(rank.contributor_id, rank.country_id): rank
for rank in ContributorRank.objects.all()
}
countries = Country.objects.all()

for contribution in contributions:
# Each contribution counts towards the rank in the country in which
# it was made, as well as the global rank for that contributor.
for country_id in (contribution.country_id, None):

contribution_country = sorted([
(country.geometry.distance(contribution.point), country)
for country in countries
])[0][1]

for country_id in (contribution_country.id, None):
rank_key = (contribution.contributor_id, country_id)
contributor_rank = contributor_ranks.get(rank_key, None)

Expand Down Expand Up @@ -146,7 +154,7 @@ class Contribution(models.Model):
A contribution made by a contributor to the leaderboard.
"""
date = models.DateField()
country = models.ForeignKey(Country)
point = models.PointField(srid=settings.WGS84_SRID, blank=True, null=True)
contributor = models.ForeignKey(Contributor)
observations = models.PositiveIntegerField(default=0)

Expand Down
12 changes: 5 additions & 7 deletions leaderboard/contributors/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from django.contrib.gis.geos import Point
from rest_framework import serializers

from leaderboard.locations.models import Country
from leaderboard.contributors.models import Contribution


Expand All @@ -20,19 +19,18 @@ class ContributionSerializer(serializers.Serializer):
observations = serializers.IntegerField()

def create(self, data):
date = datetime.datetime.fromtimestamp(data['time']).date()

country = Country.objects.nearest_to_point(Point(
point = Point(
data['tile_easting_m'],
data['tile_northing_m'],
srid=settings.PROJECTION_SRID,
))
)
point.transform(settings.WGS84_SRID)

Contribution.objects.create(
date=date,
country=country,
contributor=self.context['request'].user,
date=datetime.datetime.fromtimestamp(data['time']).date(),
observations=data['observations'],
point=point,
)

return data
9 changes: 6 additions & 3 deletions leaderboard/contributors/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
import uuid

import factory
from faker import Factory as FakerFactory
from django.conf import settings
from django.contrib.gis.geos import Point
from django.test import TestCase
from faker import Factory as FakerFactory

from leaderboard.contributors.models import (
Contributor,
Expand All @@ -28,7 +30,8 @@ class Meta:
class ContributionFactory(factory.DjangoModelFactory):
date = factory.LazyAttribute(lambda o: datetime.date.today())
contributor = factory.SubFactory(ContributorFactory)
country = factory.SubFactory(CountryFactory)
point = factory.LazyAttribute(
lambda o: Point(0, 0, srid=settings.PROJECTION_SRID))
observations = 1

class Meta:
Expand All @@ -42,7 +45,7 @@ def create_contribution(self, contributor, country):
contributor=contributor,
date=datetime.date.today(),
observations=1,
country=country,
point=country.geometry.centroid,
)

def setUp(self):
Expand Down
27 changes: 16 additions & 11 deletions leaderboard/contributors/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,35 +28,36 @@ def test_submit_multiple_observations(self):
today = datetime.date.today()
now = time.mktime(today.timetuple())
one_day = 24 * 60 * 60
point = Point(-8872100, 5435700, srid=settings.PROJECTION_SRID)

observation_data = {
'items': [
# A contribution for tile1 at time1
{
'time': now,
'tile_easting_m': -8872100,
'tile_northing_m': 5435700,
'tile_easting_m': point.coords[0],
'tile_northing_m': point.coords[1],
'observations': 100,
},
# A contribution for tile1 at time 1
{
'time': now,
'tile_easting_m': -8872100,
'tile_northing_m': 5435700,
'tile_easting_m': point.coords[0],
'tile_northing_m': point.coords[1],
'observations': 100,
},
# A contribution for tile2 at time1
{
'time': now,
'tile_easting_m': -8892100,
'tile_northing_m': 5435700,
'tile_easting_m': point.coords[0],
'tile_northing_m': point.coords[1],
'observations': 100,
},
# A contribution for tile2 at time2
{
'time': now + one_day,
'tile_easting_m': -8892100,
'tile_northing_m': 5435700,
'tile_easting_m': point.coords[0],
'tile_northing_m': point.coords[1],
'observations': 100,
},
],
Expand All @@ -75,16 +76,18 @@ def test_submit_multiple_observations(self):

self.assertEqual(Contribution.objects.count(), 4)

point.transform(settings.WGS84_SRID)

self.assertEqual(Contribution.objects.filter(date=today).count(), 3)
for contribution in Contribution.objects.filter(date=today):
self.assertEqual(contribution.contributor, self.contributor)
self.assertEqual(contribution.country, self.country)
self.assertEqual(contribution.point, point)
self.assertEqual(contribution.observations, 100)

contribution = Contribution.objects.get(
date=(today + datetime.timedelta(days=1)))
self.assertEqual(contribution.contributor, self.contributor)
self.assertEqual(contribution.country, self.country)
self.assertEqual(contribution.point, point)
self.assertEqual(contribution.observations, 100)

def test_position_converted_to_lat_lon(self):
Expand Down Expand Up @@ -142,8 +145,10 @@ def test_position_converted_to_lat_lon(self):

self.assertEqual(response.status_code, 201)

sample_point.transform(settings.WGS84_SRID)

contribution = Contribution.objects.get()
self.assertEqual(contribution.country, country1)
self.assertEqual(contribution.point, sample_point)

def test_missing_authentication_token_returns_401(self):
response = self.client.post(
Expand Down
18 changes: 9 additions & 9 deletions leaderboard/leaders/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def test_get_leader_profile_returns_ranks_and_observations(self):
contributor=contributor,
date=today,
observations=1,
country=country,
point=country.geometry.centroid,
)

# Create the contributor ranks
Expand Down Expand Up @@ -95,7 +95,7 @@ def test_sums_for_each_contributor_and_orders_by_observations(self):
contributor=contributor1,
date=today,
observations=1,
country=country1,
point=country1.geometry.centroid,
).save()

contributor2 = ContributorFactory()
Expand All @@ -105,7 +105,7 @@ def test_sums_for_each_contributor_and_orders_by_observations(self):
contributor=contributor2,
date=today,
observations=1,
country=country2,
point=country2.geometry.centroid,
).save()

# Create a contributor with no contributions
Expand Down Expand Up @@ -149,7 +149,7 @@ def test_leaderboard_is_paginated(self):
contributor=contributor,
date=today,
observations=1,
country=country,
point=country.geometry.centroid,
).save()

# Create the contributor ranks
Expand Down Expand Up @@ -181,7 +181,7 @@ def test_leader_with_no_name_not_shown(self):
contributor=contributor1,
date=today,
observations=1,
country=country,
point=country.geometry.centroid,
).save()

contributor2 = ContributorFactory(name='')
Expand All @@ -191,7 +191,7 @@ def test_leader_with_no_name_not_shown(self):
contributor=contributor2,
date=today,
observations=1,
country=country,
point=country.geometry.centroid,
).save()

# Create a contributor with no contributions
Expand Down Expand Up @@ -232,23 +232,23 @@ def test_filters_by_country(self):
contributor=contributor,
date=today,
observations=1,
country=country1,
point=country1.geometry.centroid,
)
contribution1.save()

contribution2 = Contribution(
contributor=contributor,
date=today,
observations=1,
country=country1,
point=country1.geometry.centroid,
)
contribution2.save()

contribution3 = Contribution(
contributor=contributor,
date=today,
observations=1,
country=country2,
point=country2.geometry.centroid,
)
contribution3.save()

Expand Down
18 changes: 1 addition & 17 deletions leaderboard/locations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,6 @@ def annotate_observations(self):
).filter(observations__gt=0)


class CountryManager(models.GeoManager):
"""
A model manager for Countries which allows you to
query countries which are closest to a point.
"""

def get_queryset(self):
return CountryQuerySet(self.model, using=self._db)

def nearest_to_point(self, point):
country = self.get_queryset().distance(point).order_by('distance')[:1]

if country.exists():
return country.get()


class Country(models.Model):
"""
A country as defined by:
Expand All @@ -49,7 +33,7 @@ class Country(models.Model):
subregion = models.IntegerField('Sub-Region Code')
geometry = models.MultiPolygonField(srid=settings.WGS84_SRID)

objects = CountryManager()
objects = CountryQuerySet.as_manager()

class Meta:
ordering = ('name',)
Expand Down
24 changes: 0 additions & 24 deletions leaderboard/locations/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import factory
from django.contrib.gis.geos import Point, Polygon, MultiPolygon
from django.test import TestCase

from leaderboard.locations.models import Country

Expand All @@ -29,26 +28,3 @@ class CountryFactory(factory.DjangoModelFactory):

class Meta:
model = Country


class TestCountryManager(TestCase):

def test_nearest_to_point_returns_nearest_country(self):
country1 = CountryFactory.build()
country1.save()

country2 = CountryFactory.build()
country2.geometry = MultiPolygon([
Polygon([
Point(1000, 1000),
Point(1000, 1001),
Point(1001, 1000),
Point(1001, 1001),
Point(1000, 1000),
]),
])
country2.save()

point = Point(0, 0)
nearest_country = Country.objects.nearest_to_point(point)
self.assertEqual(nearest_country, country1)
2 changes: 1 addition & 1 deletion leaderboard/locations/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def test_list_countries_returns_country_data(self):
for contribution_i in range(10):
Contribution.objects.create(
contributor=contributor,
country=country,
point=country.geometry.centroid,
date=today,
observations=1,
)
Expand Down

0 comments on commit 18ac116

Please sign in to comment.