keywrods libarry update

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-18 17:57:56 +00:00
parent 9e88c475f7
commit 43df7af989
16 changed files with 1428 additions and 45 deletions

View File

@@ -32,7 +32,7 @@ router.register(r'plans', PlanViewSet, basename='plan')
router.register(r'sites', SiteViewSet, basename='site')
router.register(r'sectors', SectorViewSet, basename='sector')
router.register(r'industries', IndustryViewSet, basename='industry')
router.register(r'seed-keywords', SeedKeywordViewSet, basename='seed-keyword')
router.register(r'keywords-library', SeedKeywordViewSet, basename='keywords-library')
# Note: AuthViewSet removed - using direct APIView endpoints instead (login, register, etc.)

View File

@@ -1064,6 +1064,350 @@ class SeedKeywordViewSet(viewsets.ReadOnlyModelViewSet):
request=request
)
@action(detail=False, methods=['get'], url_path='sector_stats', url_name='sector_stats')
def sector_stats(self, request):
"""
Get sector-level statistics for the Keywords Library dashboard.
Returns 6 stat types with dynamic fallback thresholds.
Stats:
- total: Total keywords in sector
- available: Keywords not yet added by user's site
- high_volume: Volume >= 10K (Premium Traffic)
- premium_traffic: Volume >= 50K with fallbacks (50K -> 25K -> 10K)
- long_tail: 4+ words with Volume > threshold (1K -> 500 -> 200)
- quick_wins: Difficulty <= 20, Volume > threshold, AND available
"""
from django.db.models import Count, Sum, Q, F
from django.db.models.functions import Length
try:
# Get filters
industry_id = request.query_params.get('industry_id')
sector_id = request.query_params.get('sector_id')
site_id = request.query_params.get('site_id')
if not industry_id:
return error_response(
error='industry_id is required',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
# Base queryset for the industry
base_qs = SeedKeyword.objects.filter(
is_active=True,
industry_id=industry_id
)
if sector_id:
base_qs = base_qs.filter(sector_id=sector_id)
# Get already-added keyword IDs if site_id provided
already_added_ids = set()
if site_id:
from igny8_core.business.models import SiteKeyword
already_added_ids = set(
SiteKeyword.objects.filter(
site_id=site_id,
seed_keyword__isnull=False
).values_list('seed_keyword_id', flat=True)
)
# Helper to count with availability filter
def count_available(qs):
if not site_id:
return qs.count()
return qs.exclude(id__in=already_added_ids).count()
# Helper for dynamic threshold fallback
def get_count_with_fallback(qs, thresholds, volume_field='volume'):
"""Try thresholds in order, return first with results."""
for threshold in thresholds:
filtered = qs.filter(**{f'{volume_field}__gte': threshold})
count = filtered.count()
if count > 0:
return {'count': count, 'threshold': threshold}
return {'count': 0, 'threshold': thresholds[-1]}
# 1. Total keywords
total_count = base_qs.count()
# 2. Available keywords (not yet added)
available_count = count_available(base_qs)
# 3. High Volume (>= 10K) - simple threshold
high_volume_count = base_qs.filter(volume__gte=10000).count()
# 4. Premium Traffic with dynamic fallback (50K -> 25K -> 10K)
premium_thresholds = [50000, 25000, 10000]
premium_result = get_count_with_fallback(base_qs, premium_thresholds)
# 5. Long Tail: 4+ words AND volume > threshold (1K -> 500 -> 200)
# Count words by counting spaces + 1
long_tail_base = base_qs.annotate(
word_count=Length('keyword') - Length('keyword', output_field=None) + 1
)
# Simpler: filter keywords with 3+ spaces (4+ words)
long_tail_base = base_qs.filter(keyword__regex=r'^(\S+\s+){3,}\S+$')
long_tail_thresholds = [1000, 500, 200]
long_tail_result = get_count_with_fallback(long_tail_base, long_tail_thresholds)
# 6. Quick Wins: Difficulty <= 20 AND volume > threshold AND available
quick_wins_base = base_qs.filter(difficulty__lte=20)
if site_id:
quick_wins_base = quick_wins_base.exclude(id__in=already_added_ids)
quick_wins_thresholds = [1000, 500, 200]
quick_wins_result = get_count_with_fallback(quick_wins_base, quick_wins_thresholds)
# Build response per sector if no sector_id, or single stats if sector_id provided
if sector_id:
data = {
'sector_id': int(sector_id),
'stats': {
'total': {'count': total_count},
'available': {'count': available_count},
'high_volume': {'count': high_volume_count, 'threshold': 10000},
'premium_traffic': premium_result,
'long_tail': long_tail_result,
'quick_wins': quick_wins_result,
}
}
else:
# Get stats per sector in the industry
sectors = IndustrySector.objects.filter(industry_id=industry_id)
sectors_data = []
for sector in sectors:
sector_qs = base_qs.filter(sector=sector)
sector_total = sector_qs.count()
if sector_total == 0:
continue
sector_available = count_available(sector_qs)
sector_high_volume = sector_qs.filter(volume__gte=10000).count()
sector_premium = get_count_with_fallback(sector_qs, premium_thresholds)
sector_long_tail_base = sector_qs.filter(keyword__regex=r'^(\S+\s+){3,}\S+$')
sector_long_tail = get_count_with_fallback(sector_long_tail_base, long_tail_thresholds)
sector_quick_wins_base = sector_qs.filter(difficulty__lte=20)
if site_id:
sector_quick_wins_base = sector_quick_wins_base.exclude(id__in=already_added_ids)
sector_quick_wins = get_count_with_fallback(sector_quick_wins_base, quick_wins_thresholds)
sectors_data.append({
'sector_id': sector.id,
'sector_name': sector.name,
'stats': {
'total': {'count': sector_total},
'available': {'count': sector_available},
'high_volume': {'count': sector_high_volume, 'threshold': 10000},
'premium_traffic': sector_premium,
'long_tail': sector_long_tail,
'quick_wins': sector_quick_wins,
}
})
data = {
'industry_id': int(industry_id),
'sectors': sectors_data,
}
return success_response(data=data, request=request)
except Exception as e:
return error_response(
error=f'Failed to fetch sector stats: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
@action(detail=False, methods=['get'], url_path='filter_options', url_name='filter_options')
def filter_options(self, request):
"""
Get cascading filter options for Keywords Library.
Returns industries, sectors (filtered by industry), and available filter values.
"""
from django.db.models import Count, Min, Max
try:
industry_id = request.query_params.get('industry_id')
# Get industries with keyword counts
industries = Industry.objects.annotate(
keyword_count=Count('seed_keywords', filter=Q(seed_keywords__is_active=True))
).filter(keyword_count__gt=0).order_by('name')
industries_data = [{
'id': ind.id,
'name': ind.name,
'slug': ind.slug,
'keyword_count': ind.keyword_count,
} for ind in industries]
# Get sectors filtered by industry if provided
sectors_data = []
if industry_id:
sectors = IndustrySector.objects.filter(
industry_id=industry_id
).annotate(
keyword_count=Count('seed_keywords', filter=Q(seed_keywords__is_active=True))
).filter(keyword_count__gt=0).order_by('name')
sectors_data = [{
'id': sec.id,
'name': sec.name,
'slug': sec.slug,
'keyword_count': sec.keyword_count,
} for sec in sectors]
# Get difficulty range
difficulty_range = SeedKeyword.objects.filter(is_active=True).aggregate(
min_difficulty=Min('difficulty'),
max_difficulty=Max('difficulty')
)
# Get volume range
volume_range = SeedKeyword.objects.filter(is_active=True).aggregate(
min_volume=Min('volume'),
max_volume=Max('volume')
)
# Difficulty levels for frontend (maps to backend values)
difficulty_levels = [
{'level': 1, 'label': 'Very Easy', 'backend_range': [0, 20]},
{'level': 2, 'label': 'Easy', 'backend_range': [21, 40]},
{'level': 3, 'label': 'Medium', 'backend_range': [41, 60]},
{'level': 4, 'label': 'Hard', 'backend_range': [61, 80]},
{'level': 5, 'label': 'Very Hard', 'backend_range': [81, 100]},
]
data = {
'industries': industries_data,
'sectors': sectors_data,
'difficulty': {
'range': difficulty_range,
'levels': difficulty_levels,
},
'volume': volume_range,
}
return success_response(data=data, request=request)
except Exception as e:
return error_response(
error=f'Failed to fetch filter options: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
@action(detail=False, methods=['post'], url_path='bulk_add', url_name='bulk_add')
def bulk_add(self, request):
"""
Bulk add keywords to a site from the Keywords Library.
Accepts a list of seed_keyword IDs and adds them to the specified site.
"""
from django.db import transaction
from igny8_core.business.models import SiteKeyword
try:
site_id = request.data.get('site_id')
keyword_ids = request.data.get('keyword_ids', [])
if not site_id:
return error_response(
error='site_id is required',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
if not keyword_ids or not isinstance(keyword_ids, list):
return error_response(
error='keyword_ids must be a non-empty list',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
# Verify site access
from igny8_core.business.models import Site
site = Site.objects.filter(id=site_id).first()
if not site:
return error_response(
error='Site not found',
status_code=status.HTTP_404_NOT_FOUND,
request=request
)
# Check user has access to this site
user = request.user
if not user.is_authenticated:
return error_response(
error='Authentication required',
status_code=status.HTTP_401_UNAUTHORIZED,
request=request
)
# Allow if user owns the site or is staff
if not (user.is_staff or site.account_id == getattr(user, 'account_id', None)):
return error_response(
error='Access denied to this site',
status_code=status.HTTP_403_FORBIDDEN,
request=request
)
# Get seed keywords
seed_keywords = SeedKeyword.objects.filter(
id__in=keyword_ids,
is_active=True
)
# Get already existing
existing_seed_ids = set(
SiteKeyword.objects.filter(
site_id=site_id,
seed_keyword_id__in=keyword_ids
).values_list('seed_keyword_id', flat=True)
)
added_count = 0
skipped_count = 0
with transaction.atomic():
for seed_kw in seed_keywords:
if seed_kw.id in existing_seed_ids:
skipped_count += 1
continue
SiteKeyword.objects.create(
site=site,
keyword=seed_kw.keyword,
seed_keyword=seed_kw,
volume=seed_kw.volume,
difficulty=seed_kw.difficulty,
source='library',
is_active=True
)
added_count += 1
return success_response(
data={
'added': added_count,
'skipped': skipped_count,
'total_requested': len(keyword_ids),
},
message=f'Successfully added {added_count} keywords to your site',
request=request
)
except Exception as e:
return error_response(
error=f'Failed to bulk add keywords: {str(e)}',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
# ============================================================================
# AUTHENTICATION ENDPOINTS (Register, Login, Change Password, Me)