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