keywrods libarry update
This commit is contained in:
@@ -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.)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -833,7 +833,7 @@ UNFOLD = {
|
||||
"items": [
|
||||
{"title": "Industries", "icon": "factory", "link": lambda request: "/admin/igny8_core_auth/industry/"},
|
||||
{"title": "Industry Sectors", "icon": "domain", "link": lambda request: "/admin/igny8_core_auth/industrysector/"},
|
||||
{"title": "Seed Keywords", "icon": "eco", "link": lambda request: "/admin/igny8_core_auth/seedkeyword/"},
|
||||
{"title": "Keywords Library", "icon": "eco", "link": lambda request: "/admin/igny8_core_auth/seedkeyword/"},
|
||||
],
|
||||
},
|
||||
# Trash (Soft-Deleted Records)
|
||||
|
||||
Reference in New Issue
Block a user