Skip to content

Commit

Permalink
Fixed #35841 -- Restored support for DB-IP databases in GeoIP2.
Browse files Browse the repository at this point in the history
Thanks Felix Farquharson for the report and Claude Paroz for the
review.

Regression in 40b5b15.

Co-authored-by: Natalia <[email protected]>
  • Loading branch information
ngnpope and nessita committed Oct 18, 2024
1 parent 5873f10 commit 3fad712
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 8 deletions.
28 changes: 23 additions & 5 deletions django/contrib/gis/geoip2.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@
__all__ += ["GeoIP2", "GeoIP2Exception"]


# These are the values stored in the `database_type` field of the metadata.
# See https://maxmind.github.io/MaxMind-DB/#database_type for details.
SUPPORTED_DATABASE_TYPES = {
"DBIP-City-Lite",
"DBIP-Country-Lite",
"GeoIP2-City",
"GeoIP2-Country",
"GeoLite2-City",
"GeoLite2-Country",
}


class GeoIP2Exception(Exception):
pass

Expand Down Expand Up @@ -106,7 +118,7 @@ def __init__(self, path=None, cache=0, country=None, city=None):
)

database_type = self._metadata.database_type
if not database_type.endswith(("City", "Country")):
if database_type not in SUPPORTED_DATABASE_TYPES:
raise GeoIP2Exception(f"Unable to handle database edition: {database_type}")

def __del__(self):
Expand All @@ -123,16 +135,22 @@ def __repr__(self):
def _metadata(self):
return self._reader.metadata()

@cached_property
def is_city(self):
return "City" in self._metadata.database_type

@cached_property
def is_country(self):
return "Country" in self._metadata.database_type

def _query(self, query, *, require_city=False):
if not isinstance(query, (str, ipaddress.IPv4Address, ipaddress.IPv6Address)):
raise TypeError(
"GeoIP query must be a string or instance of IPv4Address or "
"IPv6Address, not type %s" % type(query).__name__,
)

is_city = self._metadata.database_type.endswith("City")

if require_city and not is_city:
if require_city and not self.is_city:
raise GeoIP2Exception(f"Invalid GeoIP city data file: {self._path}")

try:
Expand All @@ -141,7 +159,7 @@ def _query(self, query, *, require_city=False):
# GeoIP2 only takes IP addresses, so try to resolve a hostname.
query = socket.gethostbyname(query)

function = self._reader.city if is_city else self._reader.country
function = self._reader.city if self.is_city else self._reader.country
return function(query)

def city(self, query):
Expand Down
3 changes: 3 additions & 0 deletions docs/releases/5.1.3.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ Bugfixes
:class:`~django.core.validators.DomainNameValidator` accepted any input value
that contained a valid domain name, rather than only input values that were a
valid domain name (:ticket:`35845`).

* Fixed a regression in Django 5.1 that prevented the use of DB-IP databases
with :class:`~django.contrib.gis.geoip2.GeoIP2` (:ticket:`35841`).
14 changes: 14 additions & 0 deletions tests/gis_tests/data/geoip2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,17 @@ Updates can be found in [this repository][1].

[0]: https://github.com/maxmind/MaxMind-DB/blob/main/LICENSE-MIT
[1]: https://github.com/maxmind/MaxMind-DB/tree/main/test-data

# DB-IP Lite Test Databases

The following test databases are provided under [this license][2]:

- `dbip-city-lite-test.mmdb`
- `dbip-country-lite-test.mmdb`

They have been modified to strip them down to a minimal dataset for testing.

Updates can be found at [this download page][3] from DB-IP.

[2]: https://creativecommons.org/licenses/by/4.0/
[3]: https://db-ip.com/db/lite.php
Binary file not shown.
Binary file not shown.
30 changes: 27 additions & 3 deletions tests/gis_tests/test_geoip2.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ def test_bad_query(self):

def test_country(self):
g = GeoIP2(city="<invalid>")
self.assertIs(g._metadata.database_type.endswith("Country"), True)
self.assertIs(g.is_city, False)
self.assertIs(g.is_country, True)
for query in self.query_values:
with self.subTest(query=query):
self.assertEqual(g.country(query), self.expected_country)
Expand All @@ -137,7 +138,8 @@ def test_country(self):

def test_country_using_city_database(self):
g = GeoIP2(country="<invalid>")
self.assertIs(g._metadata.database_type.endswith("City"), True)
self.assertIs(g.is_city, True)
self.assertIs(g.is_country, False)
for query in self.query_values:
with self.subTest(query=query):
self.assertEqual(g.country(query), self.expected_country)
Expand All @@ -150,7 +152,8 @@ def test_country_using_city_database(self):

def test_city(self):
g = GeoIP2(country="<invalid>")
self.assertIs(g._metadata.database_type.endswith("City"), True)
self.assertIs(g.is_city, True)
self.assertIs(g.is_country, False)
for query in self.query_values:
with self.subTest(query=query):
self.assertEqual(g.city(query), self.expected_city)
Expand Down Expand Up @@ -224,6 +227,27 @@ class GeoIP2Test(GeoLite2Test):
"""Non-free GeoIP2 databases are supported."""


@skipUnless(HAS_GEOIP2, "GeoIP2 is required.")
@override_settings(
GEOIP_CITY="dbip-city-lite-test.mmdb",
GEOIP_COUNTRY="dbip-country-lite-test.mmdb",
)
class DBIPLiteTest(GeoLite2Test):
"""DB-IP Lite databases are supported."""

expected_city = GeoLite2Test.expected_city | {
"accuracy_radius": None,
"city": "London (Shadwell)",
"latitude": 51.5181,
"longitude": -0.0714189,
"postal_code": None,
"region_code": None,
"time_zone": None,
# Kept for backward compatibility.
"region": None,
}


@skipUnless(HAS_GEOIP2, "GeoIP2 is required.")
class ErrorTest(SimpleTestCase):
def test_missing_path(self):
Expand Down

0 comments on commit 3fad712

Please sign in to comment.