Browse Source
feat(location): implement geolocation features using GeoIP2 database
feat(location): implement geolocation features using GeoIP2 database
- Added `city_detection_ip.py` for optimized location detection by coordinates and IP. - Introduced caching for location results to improve performance. - Updated `LocationHistoryView` to utilize new geolocation methods. - Added `RegionInfoView` for retrieving region information based on IP and user agent. - Included new dependencies `geoip2` and `geopy` in `requirements.txt`. - Created GeoLite2 database files for city and country data. - Updated URL patterns to include a new endpoint for region information.master
7 changed files with 429 additions and 2 deletions
-
1apps/account/urls.py
-
172apps/account/views/location_history.py
-
179city_detection_ip.py
-
77docs/CATEGORY_FILTER_EXAMPLES.md
-
2requirements.txt
-
BINutils/country_city_db/GeoLite2-City.mmdb
-
BINutils/country_city_db/GeoLite2-Country.mmdb
@ -0,0 +1,179 @@ |
|||
import geoip2.database |
|||
from pathlib import Path |
|||
from django.db import connection |
|||
from django.core.cache import cache |
|||
import logging |
|||
|
|||
# Configure logging |
|||
logger = logging.getLogger(__name__) |
|||
|
|||
# GeoLite2 database paths |
|||
CITY_DB_PATH = Path("utils/country_city_db/GeoLite2-City.mmdb") |
|||
|
|||
# Special coordinates |
|||
SPECIAL_COORDINATES = [ |
|||
(32.616565, 44.03462), |
|||
(51.5287718, -0.2416802), |
|||
(40.3947021, 49.78492), |
|||
(55.751199, 37.614706), |
|||
(48.8589466, 2.2769956), |
|||
(40.4381311, -3.8196194), |
|||
(-6.2295712, 106.759478), |
|||
(33.6158004, 72.8059198) |
|||
] |
|||
|
|||
def get_location_by_coordinates_optimized(lat, lon): |
|||
""" |
|||
Optimized version with special coordinates handling. |
|||
Handles special coordinates correctly before geo lookup. |
|||
""" |
|||
try: |
|||
# Quick validation |
|||
if not lat or not lon: |
|||
return None |
|||
|
|||
lat, lon = float(lat), float(lon) |
|||
|
|||
# Check if coordinates are in special list - should use IP detection instead |
|||
for special_lat, special_lon in SPECIAL_COORDINATES: |
|||
if abs(lat - special_lat) < 0.001 and abs(lon - special_lon) < 0.001: |
|||
# These coordinates should use IP detection, not geo lookup |
|||
# Return None to trigger fallback to original method |
|||
logger.debug(f"Special coordinate detected: ({lat}, {lon}) - skipping geo lookup") |
|||
return None |
|||
|
|||
# Simple cache key |
|||
cache_key = f'geo_{round(lat, 2)}_{round(lon, 2)}' |
|||
|
|||
# Try cache first (no exception handling for speed) |
|||
cached_result = cache.get(cache_key) |
|||
if cached_result is not None: |
|||
return cached_result |
|||
|
|||
# Simple bounding box (larger range for better coverage) |
|||
lat_range = 3.0 # ~330km |
|||
lon_range = 3.0 |
|||
|
|||
lat_min = lat - lat_range |
|||
lat_max = lat + lat_range |
|||
lon_min = lon - lon_range |
|||
lon_max = lon + lon_range |
|||
|
|||
# Simplified query - remove population filter for better accuracy |
|||
with connection.cursor() as cursor: |
|||
cursor.execute(""" |
|||
WITH bounded_cities AS ( |
|||
SELECT name, country_code, latitude, longitude |
|||
FROM geonames_city |
|||
WHERE feature_class = 'P' |
|||
AND latitude BETWEEN %s AND %s |
|||
AND longitude BETWEEN %s AND %s |
|||
), |
|||
distance_calc AS ( |
|||
SELECT name, country_code, |
|||
(6371 * acos(least(1, greatest(-1, |
|||
cos(radians(%s)) * cos(radians(latitude)) * |
|||
cos(radians(longitude) - radians(%s)) + |
|||
sin(radians(%s)) * sin(radians(latitude)) |
|||
)))) AS distance |
|||
FROM bounded_cities |
|||
) |
|||
SELECT name, country_code |
|||
FROM distance_calc |
|||
WHERE distance <= 300 |
|||
ORDER BY distance |
|||
LIMIT 1 |
|||
""", [lat_min, lat_max, lon_min, lon_max, lat, lon, lat]) |
|||
|
|||
result = cursor.fetchone() |
|||
|
|||
if result: |
|||
name, country_code = result |
|||
response = { |
|||
'status': 'success', |
|||
'city': name, |
|||
'countryCode': country_code |
|||
} |
|||
|
|||
# Cache for 24 hours |
|||
cache.set(cache_key, response, 86400) |
|||
return response |
|||
else: |
|||
# Cache None for 1 hour |
|||
cache.set(cache_key, None, 3600) |
|||
return None |
|||
|
|||
except Exception: |
|||
# Fallback to original method on any error |
|||
return get_location_by_coordinates_original(lat, lon) |
|||
|
|||
|
|||
def get_location_by_coordinates_original(lat, lon): |
|||
"""Original implementation as fallback""" |
|||
try: |
|||
with connection.cursor() as cursor: |
|||
cursor.execute(""" |
|||
WITH distance_calc AS ( |
|||
SELECT name, country_code, latitude, longitude, |
|||
(6371 * acos(least(1, greatest(-1, cos(radians(%s)) * cos(radians(latitude)) * |
|||
cos(radians(longitude) - radians(%s)) + |
|||
sin(radians(%s)) * sin(radians(latitude)))))) AS distance |
|||
FROM geonames_city |
|||
WHERE feature_class = 'P' |
|||
) |
|||
SELECT name, country_code, distance |
|||
FROM distance_calc |
|||
WHERE distance <= 300 |
|||
ORDER BY distance |
|||
LIMIT 1 |
|||
""", [lat, lon, lat]) |
|||
|
|||
result = cursor.fetchone() |
|||
|
|||
if result: |
|||
name, country_code, distance = result |
|||
return { |
|||
'status': 'success', |
|||
'city': name, |
|||
'countryCode': country_code |
|||
} |
|||
return None |
|||
|
|||
except Exception: |
|||
return None |
|||
|
|||
|
|||
def get_location_by_coordinates(lat, lon): |
|||
""" |
|||
Main function with smart fallback strategy. |
|||
Try optimized first, fallback to original if needed. |
|||
""" |
|||
# Try optimized version first |
|||
result = get_location_by_coordinates_optimized(lat, lon) |
|||
|
|||
# If optimized fails, use original as fallback |
|||
if result is None: |
|||
result = get_location_by_coordinates_original(lat, lon) |
|||
|
|||
return result |
|||
|
|||
def get_location_by_ip(ip): |
|||
"""Get location from IP using MaxMind MMDB file directly""" |
|||
try: |
|||
if not CITY_DB_PATH.exists(): |
|||
return None |
|||
|
|||
with geoip2.database.Reader(CITY_DB_PATH) as reader: |
|||
response = reader.city(ip) |
|||
if response and response.city and response.country: |
|||
return { |
|||
'status': 'success', |
|||
'countryCode': response.country.iso_code, |
|||
'city': response.city.name |
|||
} |
|||
return None |
|||
|
|||
except Exception: |
|||
return None |
|||
|
|||
|
|||
@ -0,0 +1,77 @@ |
|||
# نمونههای فیلتر دستهبندی برای Article و Book |
|||
|
|||
## Article List API |
|||
|
|||
### فیلتر با یک دستهبندی: |
|||
```bash |
|||
curl -X GET "http://localhost:8000/api/article/list/?category=islamic-history" \ |
|||
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" |
|||
``` |
|||
|
|||
### فیلتر با چند دستهبندی (جدا شده با کاما): |
|||
```bash |
|||
curl -X GET "http://localhost:8000/api/article/list/?category=islamic-history,quran-tafsir,hadith" \ |
|||
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" |
|||
``` |
|||
|
|||
### فیلتر با دستهبندی و سایر پارامترها: |
|||
```bash |
|||
curl -X GET "http://localhost:8000/api/article/list/?category=islamic-history,quran-tafsir&sort=-view_count&search=امام" \ |
|||
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Book List API |
|||
|
|||
### فیلتر با یک دستهبندی: |
|||
```bash |
|||
curl -X GET "http://localhost:8000/api/library/books/?category=fiqh" \ |
|||
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" |
|||
``` |
|||
|
|||
### فیلتر با چند دستهبندی (جدا شده با کاما): |
|||
```bash |
|||
curl -X GET "http://localhost:8000/api/library/books/?category=fiqh,aqaid,akhlaq" \ |
|||
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" |
|||
``` |
|||
|
|||
### فیلتر با دستهبندی و سایر پارامترها: |
|||
```bash |
|||
curl -X GET "http://localhost:8000/api/library/books/?category=fiqh,aqaid&sort=-download_count&search=احکام" \ |
|||
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" |
|||
``` |
|||
|
|||
### فیلتر دستهبندی + مجموعه + بوکمارک: |
|||
```bash |
|||
curl -X GET "http://localhost:8000/api/library/books/?category=fiqh&collection_id=5&is_bookmark=true" \ |
|||
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## نکات مهم: |
|||
|
|||
1. **Slug دستهبندی**: باید از slug دستهبندی استفاده کنید (نه ID) |
|||
2. **جداکننده**: از کاما (`,`) برای جدا کردن چند دستهبندی استفاده کنید |
|||
3. **فاصله**: فاصلههای قبل و بعد از کاما به صورت خودکار حذف میشوند |
|||
4. **نتیجه**: نتایج به صورت DISTINCT برگردانده میشوند (بدون تکرار) |
|||
5. **Logic**: فیلتر با منطق OR کار میکند (هر مقاله/کتابی که در یکی از دستهبندیهای داده شده باشد) |
|||
|
|||
--- |
|||
|
|||
## پارامترهای قابل ترکیب: |
|||
|
|||
### برای Article: |
|||
- `category`: slug یک یا چند دستهبندی (جدا شده با کاما) |
|||
- `collection`: slug مجموعه |
|||
- `is_bookmark`: true/false |
|||
- `search`: جستجو در عنوان |
|||
- `sort`: مرتبسازی (created_at, -created_at, view_count, -view_count, title, -title) |
|||
|
|||
### برای Book: |
|||
- `category`: slug یک یا چند دستهبندی (جدا شده با کاما) |
|||
- `collection_id`: ID مجموعه |
|||
- `is_bookmark`: true/false |
|||
- `search`: جستجو در عنوان، خلاصه، ناشر، ISBN |
|||
- `sort`: مرتبسازی (created_at, -created_at, view_count, -view_count, download_count, -download_count, title, -title, pin, -pin) |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue