diff --git a/backend/igny8_core/auth/admin.py b/backend/igny8_core/auth/admin.py index 0abff5c8..72619a07 100644 --- a/backend/igny8_core/auth/admin.py +++ b/backend/igny8_core/auth/admin.py @@ -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) diff --git a/backend/igny8_core/auth/models.py b/backend/igny8_core/auth/models.py index 0f8cfef6..7d06a4c7 100644 --- a/backend/igny8_core/auth/models.py +++ b/backend/igny8_core/auth/models.py @@ -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']), diff --git a/backend/igny8_core/auth/views.py b/backend/igny8_core/auth/views.py index a8a7a136..7592b157 100644 --- a/backend/igny8_core/auth/views.py +++ b/backend/igny8_core/auth/views.py @@ -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 + }) + diff --git a/backend/igny8_core/settings.py b/backend/igny8_core/settings.py index 2ab58ce3..f5573dc2 100644 --- a/backend/igny8_core/settings.py +++ b/backend/igny8_core/settings.py @@ -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': [ diff --git a/backend/igny8_core/templates/admin/igny8_core_auth/industry/change_list.html b/backend/igny8_core/templates/admin/igny8_core_auth/industry/change_list.html new file mode 100644 index 00000000..8a819684 --- /dev/null +++ b/backend/igny8_core/templates/admin/igny8_core_auth/industry/change_list.html @@ -0,0 +1,72 @@ +{% extends "admin/change_list.html" %} +{% load i18n admin_urls static %} + +{% block object-tools-items %} +
  • + + Import from CSV + +
  • + {{ block.super }} +{% endblock %} + +{% block content %} + + + + + {{ block.super }} +{% endblock %} diff --git a/backend/igny8_core/templates/admin/igny8_core_auth/industrysector/change_list.html b/backend/igny8_core/templates/admin/igny8_core_auth/industrysector/change_list.html new file mode 100644 index 00000000..dee937f6 --- /dev/null +++ b/backend/igny8_core/templates/admin/igny8_core_auth/industrysector/change_list.html @@ -0,0 +1,72 @@ +{% extends "admin/change_list.html" %} +{% load i18n admin_urls static %} + +{% block object-tools-items %} +
  • + + Import from CSV + +
  • + {{ block.super }} +{% endblock %} + +{% block content %} + + + + + {{ block.super }} +{% endblock %} diff --git a/backend/igny8_core/templates/admin/igny8_core_auth/seedkeyword/change_list.html b/backend/igny8_core/templates/admin/igny8_core_auth/seedkeyword/change_list.html new file mode 100644 index 00000000..f0fd10c0 --- /dev/null +++ b/backend/igny8_core/templates/admin/igny8_core_auth/seedkeyword/change_list.html @@ -0,0 +1,73 @@ +{% extends "admin/change_list.html" %} +{% load i18n admin_urls static %} + +{% block object-tools-items %} +
  • + + Import from CSV + +
  • + {{ block.super }} +{% endblock %} + +{% block content %} + + + + + {{ block.super }} +{% endblock %} diff --git a/backend/igny8_core/urls.py b/backend/igny8_core/urls.py index c1cd374f..49529308 100644 --- a/backend/igny8_core/urls.py +++ b/backend/igny8_core/urls.py @@ -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')),