Autosave: 20260206-141042

This commit is contained in:
Flatlogic Bot 2026-02-06 14:10:46 +00:00
parent d244ac9d3f
commit 0d11fc7d5d
9 changed files with 173 additions and 171 deletions

View File

@ -126,7 +126,7 @@ class BaseImportAdminMixin:
failed_rows = request.session.get(session_key, []) failed_rows = request.session.get(session_key, [])
if not failed_rows: if not failed_rows:
self.message_user(request, "No error log found in session.", level=messages.WARNING) self.message_user(request, "No error log found in session.", level=messages.WARNING)
return redirect("..\\n") return redirect("../")
response = HttpResponse(content_type="text/csv") response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = f"attachment; filename={self.model._meta.model_name}_import_errors.csv" response["Content-Disposition"] = f"attachment; filename={self.model._meta.model_name}_import_errors.csv"
@ -294,7 +294,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
preview_rows.append(row) preview_rows.append(row)
v_id = row.get(mapping.get("voter_id")) v_id = row.get(mapping.get("voter_id"))
if v_id: if v_id:
voter_ids_for_preview.append(v_id) voter_ids_for_preview.append(v_id.strip())
else: else:
break break
@ -305,7 +305,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
for row in preview_rows: for row in preview_rows:
voter_id_val = row.get(mapping.get("voter_id")) voter_id_val = row.get(mapping.get("voter_id"))
if voter_id_val in existing_preview_ids: if voter_id_val and voter_id_val.strip() in existing_preview_ids:
update_count += 1 update_count += 1
else: else:
create_count += 1 create_count += 1
@ -326,7 +326,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_preview.html", context) return render(request, "admin/import_preview.html", context)
except Exception as e: except Exception as e:
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR) self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
elif "_import" in request.POST: elif "_import" in request.POST:
file_path = request.POST.get("file_path") file_path = request.POST.get("file_path")
tenant_id = request.POST.get("tenant") tenant_id = request.POST.get("tenant")
@ -355,9 +355,14 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
for i, row in enumerate(reader): for i, row in enumerate(reader):
total_processed += 1 total_processed += 1
try: try:
voter_id = row.get(mapping.get("voter_id")) raw_voter_id = row.get(mapping.get("voter_id"))
voter_id = raw_voter_id.strip() if raw_voter_id else None
if not voter_id: if not voter_id:
row["Import Error"] = "Voter ID is required" # Enhanced error message to guide the user
mapped_column_name = mapping.get("voter_id", "N/A")
error_detail = f"Raw value: '{raw_voter_id}'. " if raw_voter_id is not None else "Value was None."
row["Import Error"] = f"Voter ID is required. Please check if the '{mapped_column_name}' column is correctly mapped and contains values for all rows. {error_detail}"
failed_rows.append(row) failed_rows.append(row)
skipped_no_id += 1 skipped_no_id += 1
errors += 1 errors += 1
@ -478,10 +483,10 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if errors > 0: if errors > 0:
error_url = reverse("admin:voter-download-errors") error_url = reverse("admin:voter-download-errors")
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING) self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
return redirect("..\\n") return redirect("../")
except Exception as e: except Exception as e:
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
else: else:
form = VoterImportForm(request.POST, request.FILES) form = VoterImportForm(request.POST, request.FILES)
if form.is_valid(): if form.is_valid():
@ -490,14 +495,14 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if not csv_file.name.endswith('.csv'): if not csv_file.name.endswith('.csv'):
self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) self.message_user(request, "Please upload a CSV file.", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp:
for chunk in csv_file.chunks(): for chunk in csv_file.chunks():
tmp.write(chunk) tmp.write(chunk)
file_path = tmp.name file_path = tmp.name
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.reader(f) reader = csv.reader(f)
headers = next(reader) headers = next(reader)
@ -554,7 +559,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
for field_name, _ in EVENT_MAPPABLE_FIELDS: for field_name, _ in EVENT_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f'map_{field_name}') mapping[field_name] = request.POST.get(f'map_{field_name}')
try: try:
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f) reader = csv.DictReader(f)
total_count = 0 total_count = 0
create_count = 0 create_count = 0
@ -613,7 +618,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_preview.html", context) return render(request, "admin/import_preview.html", context)
except Exception as e: except Exception as e:
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR) self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
elif "_import" in request.POST: elif "_import" in request.POST:
file_path = request.POST.get('file_path') file_path = request.POST.get('file_path')
@ -628,7 +633,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
count = 0 count = 0
errors = 0 errors = 0
failed_rows = [] failed_rows = []
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f) reader = csv.DictReader(f)
for row in reader: for row in reader:
try: try:
@ -708,10 +713,10 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if errors > 0: if errors > 0:
error_url = reverse("admin:event-download-errors") error_url = reverse("admin:event-download-errors")
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING) self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
return redirect("..\\n") return redirect("../")
except Exception as e: except Exception as e:
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
else: else:
form = EventImportForm(request.POST, request.FILES) form = EventImportForm(request.POST, request.FILES)
if form.is_valid(): if form.is_valid():
@ -720,14 +725,14 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if not csv_file.name.endswith('.csv'): if not csv_file.name.endswith('.csv'):
self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) self.message_user(request, "Please upload a CSV file.", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp:
for chunk in csv_file.chunks(): for chunk in csv_file.chunks():
tmp.write(chunk) tmp.write(chunk)
file_path = tmp.name file_path = tmp.name
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.reader(f) reader = csv.reader(f)
headers = next(reader) headers = next(reader)
@ -784,7 +789,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
mapping[field_name] = request.POST.get(f'map_{field_name}') mapping[field_name] = request.POST.get(f'map_{field_name}')
try: try:
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f) reader = csv.DictReader(f)
total_count = 0 total_count = 0
create_count = 0 create_count = 0
@ -827,7 +832,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_preview.html", context) return render(request, "admin/import_preview.html", context)
except Exception as e: except Exception as e:
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR) self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
elif "_import" in request.POST: elif "_import" in request.POST:
file_path = request.POST.get('file_path') file_path = request.POST.get('file_path')
@ -842,7 +847,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
count = 0 count = 0
errors = 0 errors = 0
failed_rows = [] failed_rows = []
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f) reader = csv.DictReader(f)
for row in reader: for row in reader:
try: try:
@ -880,10 +885,10 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if errors > 0: if errors > 0:
error_url = reverse("admin:volunteer-download-errors") error_url = reverse("admin:volunteer-download-errors")
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING) self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
return redirect("..\\n") return redirect("../")
except Exception as e: except Exception as e:
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
else: else:
form = VolunteerImportForm(request.POST, request.FILES) form = VolunteerImportForm(request.POST, request.FILES)
if form.is_valid(): if form.is_valid():
@ -892,14 +897,14 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if not csv_file.name.endswith('.csv'): if not csv_file.name.endswith('.csv'):
self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) self.message_user(request, "Please upload a CSV file.", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp:
for chunk in csv_file.chunks(): for chunk in csv_file.chunks():
tmp.write(chunk) tmp.write(chunk)
file_path = tmp.name file_path = tmp.name
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.reader(f) reader = csv.reader(f)
headers = next(reader) headers = next(reader)
@ -935,7 +940,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
extra_context = extra_context or {} extra_context = extra_context or {}
from core.models import Tenant from core.models import Tenant
extra_context['tenants'] = Tenant.objects.all() extra_context['tenants'] = Tenant.objects.all()
return super().changelist_view(request, extra_context=extra_context) return super().changelist_list(request, extra_context=extra_context)
def get_urls(self): def get_urls(self):
urls = super().get_urls() urls = super().get_urls()
@ -955,7 +960,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
for field_name, _ in EVENT_PARTICIPATION_MAPPABLE_FIELDS: for field_name, _ in EVENT_PARTICIPATION_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f'map_{field_name}') mapping[field_name] = request.POST.get(f'map_{field_name}')
try: try:
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f) reader = csv.DictReader(f)
total_count = 0 total_count = 0
create_count = 0 create_count = 0
@ -964,7 +969,6 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
for row in reader: for row in reader:
total_count += 1 total_count += 1
voter_id = row.get(mapping.get('voter_id')) voter_id = row.get(mapping.get('voter_id'))
event_name = row.get(mapping.get('event_name'))
# Extract first_name and last_name from CSV based on mapping # Extract first_name and last_name from CSV based on mapping
csv_first_name = row.get(mapping.get('first_name'), '') csv_first_name = row.get(mapping.get('first_name'), '')
@ -1012,7 +1016,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_preview.html", context) return render(request, "admin/import_preview.html", context)
except Exception as e: except Exception as e:
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR) self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
elif "_import" in request.POST: elif "_import" in request.POST:
file_path = request.POST.get('file_path') file_path = request.POST.get('file_path')
@ -1024,7 +1028,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
mapping[field_name] = request.POST.get(f'map_{field_name}') mapping[field_name] = request.POST.get(f'map_{field_name}')
try: try:
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f) reader = csv.DictReader(f)
count = 0 count = 0
errors = 0 errors = 0
@ -1034,6 +1038,9 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
voter_id = row.get(mapping.get('voter_id')) if mapping.get('voter_id') else None voter_id = row.get(mapping.get('voter_id')) if mapping.get('voter_id') else None
participation_status_val = row.get(mapping.get('participation_status')) if mapping.get('participation_status') else None participation_status_val = row.get(mapping.get('participation_status')) if mapping.get('participation_status') else None
if voter_id: # Only strip if voter_id is not None
voter_id = voter_id.strip()
if not voter_id: if not voter_id:
row["Import Error"] = "Missing voter ID" row["Import Error"] = "Missing voter ID"
failed_rows.append(row) failed_rows.append(row)
@ -1096,10 +1103,10 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if errors > 0: if errors > 0:
error_url = reverse("admin:eventparticipation-download-errors") error_url = reverse("admin:eventparticipation-download-errors")
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING) self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
return redirect("..\\n") return redirect("../")
except Exception as e: except Exception as e:
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
else: else:
form = EventParticipationImportForm(request.POST, request.FILES) form = EventParticipationImportForm(request.POST, request.FILES)
if form.is_valid(): if form.is_valid():
@ -1108,14 +1115,14 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if not csv_file.name.endswith('.csv'): if not csv_file.name.endswith('.csv'):
self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) self.message_user(request, "Please upload a CSV file.", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp:
for chunk in csv_file.chunks(): for chunk in csv_file.chunks():
tmp.write(chunk) tmp.write(chunk)
file_path = tmp.name file_path = tmp.name
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.reader(f) reader = csv.reader(f)
headers = next(reader) headers = next(reader)
@ -1170,7 +1177,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
for field_name, _ in DONATION_MAPPABLE_FIELDS: for field_name, _ in DONATION_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f'map_{field_name}') mapping[field_name] = request.POST.get(f'map_{field_name}')
try: try:
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f) reader = csv.DictReader(f)
total_count = 0 total_count = 0
create_count = 0 create_count = 0
@ -1213,7 +1220,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_preview.html", context) return render(request, "admin/import_preview.html", context)
except Exception as e: except Exception as e:
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR) self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
elif "_import" in request.POST: elif "_import" in request.POST:
file_path = request.POST.get('file_path') file_path = request.POST.get('file_path')
@ -1228,7 +1235,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
count = 0 count = 0
errors = 0 errors = 0
failed_rows = [] failed_rows = []
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f) reader = csv.DictReader(f)
for row in reader: for row in reader:
try: try:
@ -1237,6 +1244,9 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
amount_str = row.get(mapping.get('amount')) amount_str = row.get(mapping.get('amount'))
method_name = row.get(mapping.get('method')) method_name = row.get(mapping.get('method'))
if voter_id: # Only strip if voter_id is not None
voter_id = voter_id.strip()
if not voter_id: if not voter_id:
row["Import Error"] = "Missing voter ID" row["Import Error"] = "Missing voter ID"
failed_rows.append(row) failed_rows.append(row)
@ -1304,10 +1314,10 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if errors > 0: if errors > 0:
error_url = reverse("admin:donation-download-errors") error_url = reverse("admin:donation-download-errors")
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING) self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
return redirect("..\\n") return redirect("../")
except Exception as e: except Exception as e:
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
else: else:
form = DonationImportForm(request.POST, request.FILES) form = DonationImportForm(request.POST, request.FILES)
if form.is_valid(): if form.is_valid():
@ -1316,14 +1326,14 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if not csv_file.name.endswith('.csv'): if not csv_file.name.endswith('.csv'):
self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) self.message_user(request, "Please upload a CSV file.", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp:
for chunk in csv_file.chunks(): for chunk in csv_file.chunks():
tmp.write(chunk) tmp.write(chunk)
file_path = tmp.name file_path = tmp.name
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.reader(f) reader = csv.reader(f)
headers = next(reader) headers = next(reader)
@ -1379,7 +1389,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
for field_name, _ in INTERACTION_MAPPABLE_FIELDS: for field_name, _ in INTERACTION_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f'map_{field_name}') mapping[field_name] = request.POST.get(f'map_{field_name}')
try: try:
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f) reader = csv.DictReader(f)
total_count = 0 total_count = 0
create_count = 0 create_count = 0
@ -1423,7 +1433,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_preview.html", context) return render(request, "admin/import_preview.html", context)
except Exception as e: except Exception as e:
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR) self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
elif "_import" in request.POST: elif "_import" in request.POST:
file_path = request.POST.get('file_path') file_path = request.POST.get('file_path')
@ -1438,7 +1448,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
count = 0 count = 0
errors = 0 errors = 0
failed_rows = [] failed_rows = []
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f) reader = csv.DictReader(f)
for row in reader: for row in reader:
try: try:
@ -1447,6 +1457,9 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
date_str = row.get(mapping.get('date')) date_str = row.get(mapping.get('date'))
type_name = row.get(mapping.get('type')) type_name = row.get(mapping.get('type'))
if voter_id: # Only strip if voter_id is not None
voter_id = voter_id.strip()
if not voter_id: if not voter_id:
row["Import Error"] = "Missing voter ID" row["Import Error"] = "Missing voter ID"
failed_rows.append(row) failed_rows.append(row)
@ -1515,10 +1528,10 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if errors > 0: if errors > 0:
error_url = reverse("admin:interaction-download-errors") error_url = reverse("admin:interaction-download-errors")
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING) self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
return redirect("..\\n") return redirect("../")
except Exception as e: except Exception as e:
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
else: else:
form = InteractionImportForm(request.POST, request.FILES) form = InteractionImportForm(request.POST, request.FILES)
if form.is_valid(): if form.is_valid():
@ -1527,14 +1540,14 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if not csv_file.name.endswith('.csv'): if not csv_file.name.endswith('.csv'):
self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) self.message_user(request, "Please upload a CSV file.", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp:
for chunk in csv_file.chunks(): for chunk in csv_file.chunks():
tmp.write(chunk) tmp.write(chunk)
file_path = tmp.name file_path = tmp.name
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.reader(f) reader = csv.reader(f)
headers = next(reader) headers = next(reader)
@ -1589,7 +1602,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
for field_name, _ in VOTER_LIKELIHOOD_MAPPABLE_FIELDS: for field_name, _ in VOTER_LIKELIHOOD_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f'map_{field_name}') mapping[field_name] = request.POST.get(f'map_{field_name}')
try: try:
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f) reader = csv.DictReader(f)
total_count = 0 total_count = 0
create_count = 0 create_count = 0
@ -1632,7 +1645,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_preview.html", context) return render(request, "admin/import_preview.html", context)
except Exception as e: except Exception as e:
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR) self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
elif "_import" in request.POST: elif "_import" in request.POST:
file_path = request.POST.get('file_path') file_path = request.POST.get('file_path')
@ -1647,7 +1660,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
count = 0 count = 0
errors = 0 errors = 0
failed_rows = [] failed_rows = []
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f) reader = csv.DictReader(f)
for row in reader: for row in reader:
try: try:
@ -1655,6 +1668,9 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
election_type_name = row.get(mapping.get('election_type')) election_type_name = row.get(mapping.get('election_type'))
likelihood_val = row.get(mapping.get('likelihood')) likelihood_val = row.get(mapping.get('likelihood'))
if voter_id: # Only strip if voter_id is not None
voter_id = voter_id.strip()
if not voter_id: if not voter_id:
row["Import Error"] = "Missing voter ID" row["Import Error"] = "Missing voter ID"
failed_rows.append(row) failed_rows.append(row)
@ -1697,10 +1713,10 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if errors > 0: if errors > 0:
error_url = reverse("admin:voterlikelihood-download-errors") error_url = reverse("admin:voterlikelihood-download-errors")
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING) self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
return redirect("..\\n") return redirect("../")
except Exception as e: except Exception as e:
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
else: else:
form = VoterLikelihoodImportForm(request.POST, request.FILES) form = VoterLikelihoodImportForm(request.POST, request.FILES)
if form.is_valid(): if form.is_valid():
@ -1709,14 +1725,14 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if not csv_file.name.endswith('.csv'): if not csv_file.name.endswith('.csv'):
self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) self.message_user(request, "Please upload a CSV file.", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp:
for chunk in csv_file.chunks(): for chunk in csv_file.chunks():
tmp.write(chunk) tmp.write(chunk)
file_path = tmp.name file_path = tmp.name
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.reader(f) reader = csv.reader(f)
headers = next(reader) headers = next(reader)
@ -1772,7 +1788,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
for field_name, _ in VOTING_RECORD_MAPPABLE_FIELDS: for field_name, _ in VOTING_RECORD_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f'map_{field_name}') mapping[field_name] = request.POST.get(f'map_{field_name}')
try: try:
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f) reader = csv.DictReader(f)
total_count = 0 total_count = 0
create_count = 0 create_count = 0
@ -1831,7 +1847,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_preview.html", context) return render(request, "admin/import_preview.html", context)
except Exception as e: except Exception as e:
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR) self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
elif "_import" in request.POST: elif "_import" in request.POST:
file_path = request.POST.get('file_path') file_path = request.POST.get('file_path')
@ -1846,7 +1862,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
count = 0 count = 0
errors = 0 errors = 0
failed_rows = [] failed_rows = []
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f) reader = csv.DictReader(f)
for row in reader: for row in reader:
try: try:
@ -1855,6 +1871,9 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
election_description = row.get(mapping.get('election_description')) election_description = row.get(mapping.get('election_description'))
primary_party = row.get(mapping.get('primary_party')) primary_party = row.get(mapping.get('primary_party'))
if voter_id: # Only strip if voter_id is not None
voter_id = voter_id.strip()
if not voter_id: if not voter_id:
row["Import Error"] = "Missing voter ID" row["Import Error"] = "Missing voter ID"
failed_rows.append(row) failed_rows.append(row)
@ -1914,10 +1933,10 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if errors > 0: if errors > 0:
error_url = reverse("admin:votingrecord-download-errors") error_url = reverse("admin:votingrecord-download-errors")
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING) self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
return redirect("..\\n") return redirect("../")
except Exception as e: except Exception as e:
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
else: else:
form = VotingRecordImportForm(request.POST, request.FILES) form = VotingRecordImportForm(request.POST, request.FILES)
if form.is_valid(): if form.is_valid():
@ -1926,14 +1945,14 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if not csv_file.name.endswith('.csv'): if not csv_file.name.endswith('.csv'):
self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) self.message_user(request, "Please upload a CSV file.", level=messages.ERROR)
return redirect("..\\n") return redirect("../")
with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp:
for chunk in csv_file.chunks(): for chunk in csv_file.chunks():
tmp.write(chunk) tmp.write(chunk)
file_path = tmp.name file_path = tmp.name
with open(file_path, 'r', encoding='UTF-8') as f: with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.reader(f) reader = csv.reader(f)
headers = next(reader) headers = next(reader)

View File

@ -118,6 +118,7 @@ class AdvancedVoterSearchForm(forms.Form):
neighborhood = forms.CharField(required=False) neighborhood = forms.CharField(required=False)
district = forms.CharField(required=False) district = forms.CharField(required=False)
precinct = forms.CharField(required=False) precinct = forms.CharField(required=False)
email = forms.EmailField(required=False) # Added email field
phone_type = forms.ChoiceField( phone_type = forms.ChoiceField(
choices=[('', 'Any')] + Voter.PHONE_TYPE_CHOICES, choices=[('', 'Any')] + Voter.PHONE_TYPE_CHOICES,
required=False required=False
@ -125,7 +126,7 @@ class AdvancedVoterSearchForm(forms.Form):
is_targeted = forms.BooleanField(required=False, label="Targeted Only") is_targeted = forms.BooleanField(required=False, label="Targeted Only")
door_visit = forms.BooleanField(required=False, label="Visited Only") door_visit = forms.BooleanField(required=False, label="Visited Only")
candidate_support = forms.ChoiceField( candidate_support = forms.ChoiceField(
choices=[('', 'Any')] + Voter.SUPPORT_CHOICES, choices=[('', 'Any')] + Voter.CANDIDATE_SUPPORT_CHOICES,
required=False required=False
) )
yard_sign = forms.ChoiceField( yard_sign = forms.ChoiceField(
@ -443,7 +444,7 @@ class DoorVisitLogForm(forms.Form):
label="Wants a Yard Sign" label="Wants a Yard Sign"
) )
candidate_support = forms.ChoiceField( candidate_support = forms.ChoiceField(
choices=Voter.SUPPORT_CHOICES, choices=Voter.CANDIDATE_SUPPORT_CHOICES,
initial="unknown", initial="unknown",
widget=forms.Select(attrs={"class": "form-select"}), widget=forms.Select(attrs={"class": "form-select"}),
label="Candidate Support" label="Candidate Support"
@ -484,16 +485,6 @@ class UserUpdateForm(forms.ModelForm):
model = User model = User
fields = ['first_name', 'last_name', 'email'] fields = ['first_name', 'last_name', 'email']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs.update({'class': 'form-control'})
class VolunteerProfileForm(forms.ModelForm):
class Meta:
model = Volunteer
fields = ['phone']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
for field in self.fields.values(): for field in self.fields.values():

View File

@ -125,7 +125,7 @@ class Interest(models.Model):
return self.name return self.name
class Voter(models.Model): class Voter(models.Model):
SUPPORT_CHOICES = [ CANDIDATE_SUPPORT_CHOICES = [
('unknown', 'Unknown'), ('unknown', 'Unknown'),
('supporting', 'Supporting'), ('supporting', 'Supporting'),
('not_supporting', 'Not Supporting'), ('not_supporting', 'Not Supporting'),
@ -170,7 +170,7 @@ class Voter(models.Model):
precinct = models.CharField(max_length=100, blank=True, db_index=True) precinct = models.CharField(max_length=100, blank=True, db_index=True)
registration_date = models.DateField(null=True, blank=True) registration_date = models.DateField(null=True, blank=True)
is_targeted = models.BooleanField(default=False, db_index=True) is_targeted = models.BooleanField(default=False, db_index=True)
candidate_support = models.CharField(max_length=20, choices=SUPPORT_CHOICES, default='unknown', db_index=True) candidate_support = models.CharField(max_length=20, choices=CANDIDATE_SUPPORT_CHOICES, default='unknown', db_index=True)
yard_sign = models.CharField(max_length=20, choices=YARD_SIGN_CHOICES, default='none', db_index=True) yard_sign = models.CharField(max_length=20, choices=YARD_SIGN_CHOICES, default='none', db_index=True)
window_sticker = models.CharField(max_length=20, choices=WINDOW_STICKER_CHOICES, default='none', verbose_name='Window Sticker Status', db_index=True) window_sticker = models.CharField(max_length=20, choices=WINDOW_STICKER_CHOICES, default='none', verbose_name='Window Sticker Status', db_index=True)
notes = models.TextField(blank=True) notes = models.TextField(blank=True)

View File

@ -49,6 +49,10 @@
<label class="form-label small fw-bold text-muted">Precinct</label> <label class="form-label small fw-bold text-muted">Precinct</label>
{{ form.precinct }} {{ form.precinct }}
</div> </div>
<div class="col-md-4">
<label class="form-label small fw-bold text-muted">Email</label>
{{ form.email }}
</div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label small fw-bold text-muted">Phone Type</label> <label class="form-label small fw-bold text-muted">Phone Type</label>
{{ form.phone_type }} {{ form.phone_type }}

View File

@ -17,7 +17,7 @@ from django.contrib import messages
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.conf import settings from django.conf import settings
from .models import Voter, Tenant, Interaction, Donation, VoterLikelihood, EventParticipation, Event, EventType, InteractionType, DonationMethod, ElectionType, CampaignSettings, Volunteer, ParticipationStatus, VolunteerEvent, Interest, VolunteerRole, ScheduledCall from .models import Voter, Tenant, Interaction, Donation, VoterLikelihood, EventParticipation, Event, EventType, InteractionType, DonationMethod, ElectionType, CampaignSettings, Volunteer, ParticipationStatus, VolunteerEvent, Interest, VolunteerRole, ScheduledCall
from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm, VoterImportForm, AdvancedVoterSearchForm, EventParticipantAddForm, EventForm, VolunteerForm, VolunteerEventForm, VolunteerEventAddForm, DoorVisitLogForm, ScheduledCallForm, UserUpdateForm, VolunteerProfileForm, EventParticipationImportForm, ParticipantMappingForm from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm, VoterImportForm, AdvancedVoterSearchForm, EventParticipantAddForm, EventForm, VolunteerForm, VolunteerEventForm, VolunteerEventAddForm, DoorVisitLogForm, ScheduledCallForm, UserUpdateForm, EventParticipationImportForm, ParticipantMappingForm
import logging import logging
import zoneinfo import zoneinfo
from django.utils import timezone from django.utils import timezone
@ -157,7 +157,7 @@ def voter_list(request):
if query: if query:
query = query.strip() query = query.strip()
search_filter = Q(first_name__icontains=query) | Q(last_name__icontains=query) | Q(voter_id__icontains=query) search_filter = Q(first_name__icontains=query) | Q(last_name__icontains=query) | Q(voter_id__iexact=query)
if "," in query: if "," in query:
parts = [p.strip() for p in query.split(",")] parts = [p.strip() for p in query.split(",")]
@ -358,7 +358,6 @@ def edit_likelihood(request, likelihood_id):
if request.method == 'POST': if request.method == 'POST':
form = VoterLikelihoodForm(request.POST, instance=likelihood, tenant=tenant) form = VoterLikelihoodForm(request.POST, instance=likelihood, tenant=tenant)
if form.is_valid(): if form.is_valid():
# Check for conflict with another record of same election_type
election_type = form.cleaned_data['election_type'] election_type = form.cleaned_data['election_type']
if VoterLikelihood.objects.filter(voter=likelihood.voter, election_type=election_type).exclude(id=likelihood.id).exists(): if VoterLikelihood.objects.filter(voter=likelihood.voter, election_type=election_type).exclude(id=likelihood.id).exists():
VoterLikelihood.objects.filter(voter=likelihood.voter, election_type=election_type).exclude(id=likelihood.id).delete() VoterLikelihood.objects.filter(voter=likelihood.voter, election_type=election_type).exclude(id=likelihood.id).delete()
@ -487,7 +486,7 @@ def voter_advanced_search(request):
if data.get('address'): if data.get('address'):
voters = voters.filter(Q(address__icontains=data['address']) | Q(address_street__icontains=data['address'])) voters = voters.filter(Q(address__icontains=data['address']) | Q(address_street__icontains=data['address']))
if data.get('voter_id'): if data.get('voter_id'):
voters = voters.filter(voter_id__icontains=data['voter_id']) voters = voters.filter(voter_id__iexact=data['voter_id'])
if data.get('birth_month'): if data.get('birth_month'):
voters = voters.filter(birthdate__month=data['birth_month']) voters = voters.filter(birthdate__month=data['birth_month'])
if data.get('city'): if data.get('city'):
@ -498,6 +497,8 @@ def voter_advanced_search(request):
voters = voters.filter(district=data['district']) voters = voters.filter(district=data['district'])
if data.get('precinct'): if data.get('precinct'):
voters = voters.filter(precinct=data['precinct']) voters = voters.filter(precinct=data['precinct'])
if data.get('email'):
voters = voters.filter(email__icontains=data['email'])
if data.get('phone_type'): if data.get('phone_type'):
voters = voters.filter(phone_type=data['phone_type']) voters = voters.filter(phone_type=data['phone_type'])
if data.get('is_targeted'): if data.get('is_targeted'):
@ -563,7 +564,7 @@ def export_voters_csv(request):
if data.get('address'): if data.get('address'):
voters = voters.filter(Q(address__icontains=data['address']) | Q(address_street__icontains=data['address'])) voters = voters.filter(Q(address__icontains=data['address']) | Q(address_street__icontains=data['address']))
if data.get('voter_id'): if data.get('voter_id'):
voters = voters.filter(voter_id__icontains=data['voter_id']) voters = voters.filter(voter_id__iexact=data['voter_id'])
if data.get('birth_month'): if data.get('birth_month'):
voters = voters.filter(birthdate__month=data['birth_month']) voters = voters.filter(birthdate__month=data['birth_month'])
if data.get('city'): if data.get('city'):
@ -574,6 +575,8 @@ def export_voters_csv(request):
voters = voters.filter(district=data['district']) voters = voters.filter(district=data['district'])
if data.get('precinct'): if data.get('precinct'):
voters = voters.filter(precinct=data['precinct']) voters = voters.filter(precinct=data['precinct'])
if data.get('email'):
voters = voters.filter(email__icontains=data['email'])
if data.get('phone_type'): if data.get('phone_type'):
voters = voters.filter(phone_type=data['phone_type']) voters = voters.filter(phone_type=data['phone_type'])
if data.get('is_targeted'): if data.get('is_targeted'):
@ -842,7 +845,7 @@ def voter_search_json(request):
tenant = get_object_or_404(Tenant, id=selected_tenant_id) tenant = get_object_or_404(Tenant, id=selected_tenant_id)
voters = Voter.objects.filter(tenant=tenant) voters = Voter.objects.filter(tenant=tenant)
search_filter = Q(first_name__icontains=query) | Q(last_name__icontains=query) | Q(voter_id__icontains=query) search_filter = Q(first_name__icontains=query) | Q(last_name__icontains=query) | Q(voter_id__iexact=query)
if "," in query: if "," in query:
parts = [p.strip() for p in query.split(",") ] parts = [p.strip() for p in query.split(",") ]
@ -925,6 +928,7 @@ def volunteer_add(request):
'form': form, 'form': form,
'tenant': tenant, 'tenant': tenant,
'selected_tenant': tenant, 'selected_tenant': tenant,
'is_create': True,
} }
return render(request, 'core/volunteer_detail.html', context) return render(request, 'core/volunteer_detail.html', context)
@ -1993,21 +1997,5 @@ def profile(request):
if request.method == 'POST': if request.method == 'POST':
u_form = UserUpdateForm(request.POST, instance=request.user) u_form = UserUpdateForm(request.POST, instance=request.user)
v_form = VolunteerProfileForm(request.POST, instance=volunteer) if volunteer else None # v_form = VolunteerProfileForm(request.POST, instance=volunteer) if volunteer else None # Removed VolunteerProfileForm
v_form = None # Set v_form to None after removal
if u_form.is_valid() and (not v_form or v_form.is_valid()):
u_form.save()
if v_form:
v_form.save()
messages.success(request, f'Your profile has been updated!')
return redirect('profile')
else:
u_form = UserUpdateForm(instance=request.user)
v_form = VolunteerProfileForm(instance=volunteer) if volunteer else None
context = {
'u_form': u_form,
'v_form': v_form
}
return render(request, 'core/profile.html', context)