This commit is contained in:
IGNY8 VPS (Salman)
2025-11-30 05:06:43 +00:00
parent 8d47d6a555
commit 59e9cb4322
8 changed files with 465 additions and 2 deletions

View File

@@ -243,10 +243,16 @@ class IndustryAdmin(admin.ModelAdmin):
search_fields = ['name', 'slug', 'description']
readonly_fields = ['created_at', 'updated_at']
inlines = [IndustrySectorInline]
actions = ['delete_selected'] # Enable bulk delete
change_list_template = 'admin/igny8_core_auth/industry/change_list.html'
def get_sectors_count(self, obj):
return obj.sectors.filter(is_active=True).count()
get_sectors_count.short_description = 'Active Sectors'
def has_delete_permission(self, request, obj=None):
"""Allow deletion for superusers and developers"""
return request.user.is_superuser or (hasattr(request.user, 'is_developer') and request.user.is_developer())
@admin.register(IndustrySector)
@@ -255,6 +261,12 @@ class IndustrySectorAdmin(admin.ModelAdmin):
list_filter = ['is_active', 'industry']
search_fields = ['name', 'slug', 'description']
readonly_fields = ['created_at', 'updated_at']
actions = ['delete_selected'] # Enable bulk delete
change_list_template = 'admin/igny8_core_auth/industrysector/change_list.html'
def has_delete_permission(self, request, obj=None):
"""Allow deletion for superusers and developers"""
return request.user.is_superuser or (hasattr(request.user, 'is_developer') and request.user.is_developer())
@admin.register(SeedKeyword)
@@ -264,6 +276,8 @@ class SeedKeywordAdmin(admin.ModelAdmin):
list_filter = ['is_active', 'industry', 'sector', 'intent']
search_fields = ['keyword']
readonly_fields = ['created_at', 'updated_at']
actions = ['delete_selected'] # Enable bulk delete
change_list_template = 'admin/igny8_core_auth/seedkeyword/change_list.html'
fieldsets = (
('Keyword Info', {
@@ -276,6 +290,10 @@ class SeedKeywordAdmin(admin.ModelAdmin):
'fields': ('created_at', 'updated_at')
}),
)
def has_delete_permission(self, request, obj=None):
"""Allow deletion for superusers and developers"""
return request.user.is_superuser or (hasattr(request.user, 'is_developer') and request.user.is_developer())
@admin.register(User)

View File

@@ -379,7 +379,7 @@ class SeedKeyword(models.Model):
db_table = 'igny8_seed_keywords'
unique_together = [['keyword', 'industry', 'sector']]
verbose_name = 'Seed Keyword'
verbose_name_plural = 'Seed Keywords'
verbose_name_plural = 'Global Keywords Database'
indexes = [
models.Index(fields=['keyword']),
models.Index(fields=['industry', 'sector']),

View File

@@ -1316,3 +1316,219 @@ class AuthViewSet(viewsets.GenericViewSet):
message='Password has been reset successfully',
request=request
)
# ============================================================================
# CSV Import/Export Views for Admin
# ============================================================================
from django.http import HttpResponse, JsonResponse
from django.contrib.admin.views.decorators import staff_member_required
from django.views.decorators.http import require_http_methods
import csv
import io
@staff_member_required
@require_http_methods(["GET"])
def industry_csv_template(request):
"""Download CSV template for Industry import"""
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="industry_template.csv"'
writer = csv.writer(response)
writer.writerow(['name', 'description', 'is_active'])
writer.writerow(['Technology', 'Technology industry', 'true'])
writer.writerow(['Healthcare', 'Healthcare and medical services', 'true'])
return response
@staff_member_required
@require_http_methods(["POST"])
def industry_csv_import(request):
"""Import industries from CSV"""
if not request.FILES.get('csv_file'):
return JsonResponse({'success': False, 'error': 'No CSV file provided'}, status=400)
csv_file = request.FILES['csv_file']
decoded_file = csv_file.read().decode('utf-8')
io_string = io.StringIO(decoded_file)
reader = csv.DictReader(io_string)
created = 0
updated = 0
errors = []
from django.utils.text import slugify
for row_num, row in enumerate(reader, start=2):
try:
is_active = row.get('is_active', 'true').lower() in ['true', '1', 'yes']
slug = slugify(row['name'])
industry, created_flag = Industry.objects.update_or_create(
name=row['name'],
defaults={
'slug': slug,
'description': row.get('description', ''),
'is_active': is_active
}
)
if created_flag:
created += 1
else:
updated += 1
except Exception as e:
errors.append(f"Row {row_num}: {str(e)}")
return JsonResponse({
'success': True,
'created': created,
'updated': updated,
'errors': errors
})
@staff_member_required
@require_http_methods(["GET"])
def industrysector_csv_template(request):
"""Download CSV template for IndustrySector import"""
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="industrysector_template.csv"'
writer = csv.writer(response)
writer.writerow(['name', 'industry', 'description', 'is_active'])
writer.writerow(['Software Development', 'Technology', 'Software and app development', 'true'])
writer.writerow(['Healthcare IT', 'Healthcare', 'Healthcare information technology', 'true'])
return response
@staff_member_required
@require_http_methods(["POST"])
def industrysector_csv_import(request):
"""Import industry sectors from CSV"""
if not request.FILES.get('csv_file'):
return JsonResponse({'success': False, 'error': 'No CSV file provided'}, status=400)
csv_file = request.FILES['csv_file']
decoded_file = csv_file.read().decode('utf-8')
io_string = io.StringIO(decoded_file)
reader = csv.DictReader(io_string)
created = 0
updated = 0
errors = []
from django.utils.text import slugify
for row_num, row in enumerate(reader, start=2):
try:
is_active = row.get('is_active', 'true').lower() in ['true', '1', 'yes']
slug = slugify(row['name'])
# Find industry by name
try:
industry = Industry.objects.get(name=row['industry'])
except Industry.DoesNotExist:
errors.append(f"Row {row_num}: Industry '{row['industry']}' not found")
continue
sector, created_flag = IndustrySector.objects.update_or_create(
name=row['name'],
industry=industry,
defaults={
'slug': slug,
'description': row.get('description', ''),
'is_active': is_active
}
)
if created_flag:
created += 1
else:
updated += 1
except Exception as e:
errors.append(f"Row {row_num}: {str(e)}")
return JsonResponse({
'success': True,
'created': created,
'updated': updated,
'errors': errors
})
@staff_member_required
@require_http_methods(["GET"])
def seedkeyword_csv_template(request):
"""Download CSV template for SeedKeyword import"""
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="seedkeyword_template.csv"'
writer = csv.writer(response)
writer.writerow(['keyword', 'industry', 'sector', 'volume', 'difficulty', 'intent', 'is_active'])
writer.writerow(['python programming', 'Technology', 'Software Development', '10000', '45', 'Informational', 'true'])
writer.writerow(['medical software', 'Healthcare', 'Healthcare IT', '5000', '60', 'Commercial', 'true'])
return response
@staff_member_required
@require_http_methods(["POST"])
def seedkeyword_csv_import(request):
"""Import seed keywords from CSV"""
if not request.FILES.get('csv_file'):
return JsonResponse({'success': False, 'error': 'No CSV file provided'}, status=400)
csv_file = request.FILES['csv_file']
decoded_file = csv_file.read().decode('utf-8')
io_string = io.StringIO(decoded_file)
reader = csv.DictReader(io_string)
created = 0
updated = 0
errors = []
for row_num, row in enumerate(reader, start=2):
try:
is_active = row.get('is_active', 'true').lower() in ['true', '1', 'yes']
# Find industry and sector by name
try:
industry = Industry.objects.get(name=row['industry'])
except Industry.DoesNotExist:
errors.append(f"Row {row_num}: Industry '{row['industry']}' not found")
continue
try:
sector = IndustrySector.objects.get(name=row['sector'], industry=industry)
except IndustrySector.DoesNotExist:
errors.append(f"Row {row_num}: Sector '{row['sector']}' not found in industry '{row['industry']}'")
continue
keyword, created_flag = SeedKeyword.objects.update_or_create(
keyword=row['keyword'],
industry=industry,
sector=sector,
defaults={
'volume': int(row.get('volume', 0)),
'difficulty': int(row.get('difficulty', 0)),
'intent': row.get('intent', 'Informational'),
'is_active': is_active
}
)
if created_flag:
created += 1
else:
updated += 1
except Exception as e:
errors.append(f"Row {row_num}: {str(e)}")
return JsonResponse({
'success': True,
'created': created,
'updated': updated,
'errors': errors
})

View File

@@ -103,7 +103,7 @@ ROOT_URLCONF = 'igny8_core.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'DIRS': [BASE_DIR / 'igny8_core' / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [

View File

@@ -0,0 +1,72 @@
{% extends "admin/change_list.html" %}
{% load i18n admin_urls static %}
{% block object-tools-items %}
<li>
<a href="#" class="addlink" onclick="document.getElementById('csv-import-form').style.display='block'; return false;">
Import from CSV
</a>
</li>
{{ block.super }}
{% endblock %}
{% block content %}
<div id="csv-import-form" style="display:none; background: #f8f8f8; padding: 20px; margin: 20px 0; border: 1px solid #ddd;">
<h2>Import Industries from CSV</h2>
<form method="post" enctype="multipart/form-data" action="{% url 'admin_industry_csv_import' %}" id="csv-upload-form">
{% csrf_token %}
<p>
<label for="csv_file">Select CSV file:</label>
<input type="file" name="csv_file" id="csv_file" accept=".csv" required>
</p>
<p>
<strong>CSV Format:</strong> name, description, is_active<br>
<em>Note: Slug will be auto-generated from name</em><br>
<a href="{% url 'admin_industry_csv_template' %}" download>Download template with examples</a>
</p>
<p>
<button type="submit" class="button">Upload and Import</button>
<button type="button" class="button" onclick="document.getElementById('csv-import-form').style.display='none'">Cancel</button>
</p>
</form>
<div id="import-results"></div>
</div>
<script>
document.getElementById('csv-upload-form').addEventListener('submit', function(e) {
e.preventDefault();
var formData = new FormData(this);
var resultsDiv = document.getElementById('import-results');
resultsDiv.innerHTML = '<p>Importing...</p>';
fetch('{% url "admin_industry_csv_import" %}', {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
resultsDiv.innerHTML = '<p style="color: green;">✓ Import successful!<br>' +
'Created: ' + data.created + '<br>' +
'Updated: ' + data.updated + '<br>' +
(data.errors.length > 0 ? 'Errors: ' + data.errors.join('<br>') : '') +
'</p>';
setTimeout(function() {
window.location.reload();
}, 2000);
} else {
resultsDiv.innerHTML = '<p style="color: red;">✗ Error: ' + data.error + '</p>';
}
})
.catch(error => {
resultsDiv.innerHTML = '<p style="color: red;">✗ Error: ' + error + '</p>';
});
});
</script>
{{ block.super }}
{% endblock %}

View File

@@ -0,0 +1,72 @@
{% extends "admin/change_list.html" %}
{% load i18n admin_urls static %}
{% block object-tools-items %}
<li>
<a href="#" class="addlink" onclick="document.getElementById('csv-import-form').style.display='block'; return false;">
Import from CSV
</a>
</li>
{{ block.super }}
{% endblock %}
{% block content %}
<div id="csv-import-form" style="display:none; background: #f8f8f8; padding: 20px; margin: 20px 0; border: 1px solid #ddd;">
<h2>Import Industry Sectors from CSV</h2>
<form method="post" enctype="multipart/form-data" action="{% url 'admin_industrysector_csv_import' %}" id="csv-upload-form">
{% csrf_token %}
<p>
<label for="csv_file">Select CSV file:</label>
<input type="file" name="csv_file" id="csv_file" accept=".csv" required>
</p>
<p>
<strong>CSV Format:</strong> name, industry, description, is_active<br>
<em>Note: Use industry name (not slug). Slug will be auto-generated from name</em><br>
<a href="{% url 'admin_industrysector_csv_template' %}" download>Download template with examples</a>
</p>
<p>
<button type="submit" class="button">Upload and Import</button>
<button type="button" class="button" onclick="document.getElementById('csv-import-form').style.display='none'">Cancel</button>
</p>
</form>
<div id="import-results"></div>
</div>
<script>
document.getElementById('csv-upload-form').addEventListener('submit', function(e) {
e.preventDefault();
var formData = new FormData(this);
var resultsDiv = document.getElementById('import-results');
resultsDiv.innerHTML = '<p>Importing...</p>';
fetch('{% url "admin_industrysector_csv_import" %}', {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
resultsDiv.innerHTML = '<p style="color: green;">✓ Import successful!<br>' +
'Created: ' + data.created + '<br>' +
'Updated: ' + data.updated + '<br>' +
(data.errors.length > 0 ? 'Errors: ' + data.errors.join('<br>') : '') +
'</p>';
setTimeout(function() {
window.location.reload();
}, 2000);
} else {
resultsDiv.innerHTML = '<p style="color: red;">✗ Error: ' + data.error + '</p>';
}
})
.catch(error => {
resultsDiv.innerHTML = '<p style="color: red;">✗ Error: ' + error + '</p>';
});
});
</script>
{{ block.super }}
{% endblock %}

View File

@@ -0,0 +1,73 @@
{% extends "admin/change_list.html" %}
{% load i18n admin_urls static %}
{% block object-tools-items %}
<li>
<a href="#" class="addlink" onclick="document.getElementById('csv-import-form').style.display='block'; return false;">
Import from CSV
</a>
</li>
{{ block.super }}
{% endblock %}
{% block content %}
<div id="csv-import-form" style="display:none; background: #f8f8f8; padding: 20px; margin: 20px 0; border: 1px solid #ddd;">
<h2>Import Global Keywords from CSV</h2>
<form method="post" enctype="multipart/form-data" action="{% url 'admin_seedkeyword_csv_import' %}" id="csv-upload-form">
{% csrf_token %}
<p>
<label for="csv_file">Select CSV file:</label>
<input type="file" name="csv_file" id="csv_file" accept=".csv" required>
</p>
<p>
<strong>CSV Format:</strong> keyword, industry, sector, volume, difficulty, intent, is_active<br>
<em>Note: Use industry and sector names (not slugs)</em><br>
<strong>Intent values:</strong> Informational, Commercial, Transactional, Navigational<br>
<a href="{% url 'admin_seedkeyword_csv_template' %}" download>Download template with examples</a>
</p>
<p>
<button type="submit" class="button">Upload and Import</button>
<button type="button" class="button" onclick="document.getElementById('csv-import-form').style.display='none'">Cancel</button>
</p>
</form>
<div id="import-results"></div>
</div>
<script>
document.getElementById('csv-upload-form').addEventListener('submit', function(e) {
e.preventDefault();
var formData = new FormData(this);
var resultsDiv = document.getElementById('import-results');
resultsDiv.innerHTML = '<p>Importing...</p>';
fetch('{% url "admin_seedkeyword_csv_import" %}', {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
resultsDiv.innerHTML = '<p style="color: green;">✓ Import successful!<br>' +
'Created: ' + data.created + '<br>' +
'Updated: ' + data.updated + '<br>' +
(data.errors.length > 0 ? 'Errors: ' + data.errors.join('<br>') : '') +
'</p>';
setTimeout(function() {
window.location.reload();
}, 2000);
} else {
resultsDiv.innerHTML = '<p style="color: red;">✗ Error: ' + data.error + '</p>';
}
})
.catch(error => {
resultsDiv.innerHTML = '<p style="color: red;">✗ Error: ' + error + '</p>';
});
});
</script>
{{ block.super }}
{% endblock %}

View File

@@ -21,8 +21,20 @@ from drf_spectacular.views import (
SpectacularRedocView,
SpectacularSwaggerView,
)
from igny8_core.auth.views import (
industry_csv_template, industry_csv_import,
industrysector_csv_template, industrysector_csv_import,
seedkeyword_csv_template, seedkeyword_csv_import
)
urlpatterns = [
# CSV Import/Export for admin - MUST come before admin/ to avoid being caught by admin.site.urls
path('admin/igny8_core_auth/industry/csv-template/', industry_csv_template, name='admin_industry_csv_template'),
path('admin/igny8_core_auth/industry/csv-import/', industry_csv_import, name='admin_industry_csv_import'),
path('admin/igny8_core_auth/industrysector/csv-template/', industrysector_csv_template, name='admin_industrysector_csv_template'),
path('admin/igny8_core_auth/industrysector/csv-import/', industrysector_csv_import, name='admin_industrysector_csv_import'),
path('admin/igny8_core_auth/seedkeyword/csv-template/', seedkeyword_csv_template, name='admin_seedkeyword_csv_template'),
path('admin/igny8_core_auth/seedkeyword/csv-import/', seedkeyword_csv_import, name='admin_seedkeyword_csv_import'),
path('admin/', admin.site.urls),
path('api/v1/auth/', include('igny8_core.auth.urls')), # Auth endpoints
path('api/v1/planner/', include('igny8_core.modules.planner.urls')),