import
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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']),
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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': [
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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')),
|
||||
|
||||
Reference in New Issue
Block a user