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

@ -19,7 +19,7 @@ from .models import (
Interest, Volunteer, VolunteerEvent, ParticipationStatus, VolunteerRole Interest, Volunteer, VolunteerEvent, ParticipationStatus, VolunteerRole
) )
from .forms import ( from .forms import (
VoterImportForm, EventImportForm, EventParticipationImportForm, VoterImportForm, EventImportForm, EventParticipationImportForm,
DonationImportForm, InteractionImportForm, VoterLikelihoodImportForm, DonationImportForm, InteractionImportForm, VoterLikelihoodImportForm,
VolunteerImportForm, VotingRecordImportForm VolunteerImportForm, VotingRecordImportForm
) )
@ -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"
@ -252,7 +252,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
inlines = [VotingRecordInline, DonationInline, InteractionInline, VoterLikelihoodInline] inlines = [VotingRecordInline, DonationInline, InteractionInline, VoterLikelihoodInline]
readonly_fields = ('address',) readonly_fields = ('address',)
change_list_template = "admin/voter_change_list.html" change_list_template = "admin/voter_change_list.html"
def changelist_view(self, request, extra_context=None): def changelist_view(self, request, extra_context=None):
extra_context = extra_context or {} extra_context = extra_context or {}
from core.models import Tenant from core.models import Tenant
@ -274,7 +274,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
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")
tenant = Tenant.objects.get(id=tenant_id) tenant = Tenant.objects.get(id=tenant_id)
mapping = {} mapping = {}
for field_name, _ in VOTER_MAPPABLE_FIELDS: for field_name, _ in VOTER_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f"map_{field_name}") mapping[field_name] = request.POST.get(f"map_{field_name}")
@ -294,18 +294,18 @@ 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
existing_preview_ids = set(Voter.objects.filter(tenant=tenant, voter_id__in=voter_ids_for_preview).values_list("voter_id", flat=True)) existing_preview_ids = set(Voter.objects.filter(tenant=tenant, voter_id__in=voter_ids_for_preview).values_list("voter_id", flat=True))
create_count = 0 create_count = 0
update_count = 0 update_count = 0
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,12 +326,12 @@ 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")
tenant = Tenant.objects.get(id=tenant_id) tenant = Tenant.objects.get(id=tenant_id)
mapping = {} mapping = {}
for field_name, _ in VOTER_MAPPABLE_FIELDS: for field_name, _ in VOTER_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f"map_{field_name}") mapping[field_name] = request.POST.get(f"map_{field_name}")
@ -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
@ -446,7 +451,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
created_count += 1 created_count += 1
else: else:
updated_count += 1 updated_count += 1
# Special handling for interests - assuming a comma-separated list in CSV # Special handling for interests - assuming a comma-separated list in CSV
if 'interests' in mapping and row.get(mapping['interests']): if 'interests' in mapping and row.get(mapping['interests']):
interest_names = [name.strip() for name in row[mapping['interests']].split(',') if name.strip()] interest_names = [name.strip() for name in row[mapping['interests']].split(',') if name.strip()]
@ -478,26 +483,26 @@ 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():
csv_file = request.FILES['file'] csv_file = request.FILES['file']
tenant = form.cleaned_data['tenant'] tenant = form.cleaned_data['tenant']
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)
@ -514,7 +519,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_mapping.html", context) return render(request, "admin/import_mapping.html", context)
else: else:
form = VoterImportForm() form = VoterImportForm()
context = self.admin_site.each_context(request) context = self.admin_site.each_context(request)
context['form'] = form context['form'] = form
context['title'] = "Import Voters" context['title'] = "Import Voters"
@ -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
@ -564,7 +569,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
total_count += 1 total_count += 1
event_name = row.get(mapping.get('name')) event_name = row.get(mapping.get('name'))
event_date = row.get(mapping.get('date')) event_date = row.get(mapping.get('date'))
exists = False exists = False
if event_name and event_date: if event_name and event_date:
try: try:
@ -579,18 +584,18 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if dt: if dt:
exists = Event.objects.filter(tenant=tenant, name=event_name, date=dt).exists() exists = Event.objects.filter(tenant=tenant, name=event_name, date=dt).exists()
except ValueError: except ValueError:
# Handle cases where date parsing fails # Handle cases where date parsing fails
pass pass
if exists: if exists:
update_count += 1 update_count += 1
action = 'update' action = 'update'
else: else:
create_count += 1 create_count += 1
action = 'create' action = 'create'
if len(preview_data) < 10: if len(preview_data) < 10:
preview_data.append({ preview_data.append({
'action': action, 'action': action,
@ -613,13 +618,13 @@ 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')
tenant_id = request.POST.get('tenant') tenant_id = request.POST.get('tenant')
tenant = Tenant.objects.get(id=tenant_id) tenant = Tenant.objects.get(id=tenant_id)
mapping = {} mapping = {}
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}')
@ -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:
@ -658,7 +663,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
failed_rows.append(row) failed_rows.append(row)
errors += 1 errors += 1
continue continue
event_type_obj, _ = EventType.objects.get_or_create(tenant=tenant, name=event_type_name) event_type_obj, _ = EventType.objects.get_or_create(tenant=tenant, name=event_type_name)
defaults = { defaults = {
@ -699,7 +704,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
row["Import Error"] = str(e) row["Import Error"] = str(e)
failed_rows.append(row) failed_rows.append(row)
errors += 1 errors += 1
if os.path.exists(file_path): if os.path.exists(file_path):
os.remove(file_path) os.remove(file_path)
self.message_user(request, f"Successfully imported {count} events.") self.message_user(request, f"Successfully imported {count} events.")
@ -708,26 +713,26 @@ 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():
csv_file = request.FILES['file'] csv_file = request.FILES['file']
tenant = form.cleaned_data['tenant'] tenant = form.cleaned_data['tenant']
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)
@ -744,7 +749,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_mapping.html", context) return render(request, "admin/import_mapping.html", context)
else: else:
form = EventImportForm() form = EventImportForm()
context = self.admin_site.each_context(request) context = self.admin_site.each_context(request)
context['form'] = form context['form'] = form
context['title'] = "Import Events" context['title'] = "Import Events"
@ -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
@ -797,14 +802,14 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
exists = False exists = False
if email: if email:
exists = Volunteer.objects.filter(tenant=tenant, email=email).exists() exists = Volunteer.objects.filter(tenant=tenant, email=email).exists()
if exists: if exists:
update_count += 1 update_count += 1
action = 'update' action = 'update'
else: else:
create_count += 1 create_count += 1
action = 'create' action = 'create'
if len(preview_data) < 10: if len(preview_data) < 10:
preview_data.append({ preview_data.append({
'action': action, 'action': action,
@ -827,13 +832,13 @@ 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')
tenant_id = request.POST.get('tenant') tenant_id = request.POST.get('tenant')
tenant = Tenant.objects.get(id=tenant_id) tenant = Tenant.objects.get(id=tenant_id)
mapping = {} mapping = {}
for field_name, _ in VOLUNTEER_MAPPABLE_FIELDS: for field_name, _ in VOLUNTEER_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f'map_{field_name}') mapping[field_name] = request.POST.get(f'map_{field_name}')
@ -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:
@ -852,7 +857,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
failed_rows.append(row) failed_rows.append(row)
errors += 1 errors += 1
continue continue
defaults = { defaults = {
'first_name': row.get(mapping.get('first_name')) or '', 'first_name': row.get(mapping.get('first_name')) or '',
'last_name': row.get(mapping.get('last_name')) or '', 'last_name': row.get(mapping.get('last_name')) or '',
@ -871,7 +876,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
row["Import Error"] = str(e) row["Import Error"] = str(e)
failed_rows.append(row) failed_rows.append(row)
errors += 1 errors += 1
if os.path.exists(file_path): if os.path.exists(file_path):
os.remove(file_path) os.remove(file_path)
self.message_user(request, f"Successfully imported {count} volunteers.") self.message_user(request, f"Successfully imported {count} volunteers.")
@ -880,26 +885,26 @@ 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():
csv_file = request.FILES['file'] csv_file = request.FILES['file']
tenant = form.cleaned_data['tenant'] tenant = form.cleaned_data['tenant']
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)
@ -916,7 +921,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_mapping.html", context) return render(request, "admin/import_mapping.html", context)
else: else:
form = VolunteerImportForm() form = VolunteerImportForm()
context = self.admin_site.each_context(request) context = self.admin_site.each_context(request)
context['form'] = form context['form'] = form
context['title'] = "Import Volunteers" context['title'] = "Import Volunteers"
@ -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,8 +969,7 @@ 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'), '')
csv_last_name = row.get(mapping.get('last_name'), '') csv_last_name = row.get(mapping.get('last_name'), '')
@ -981,14 +985,14 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
exists = EventParticipation.objects.filter(voter=voter, event__name=event_name).exists() exists = EventParticipation.objects.filter(voter=voter, event__name=event_name).exists()
except Voter.DoesNotExist: except Voter.DoesNotExist:
pass pass
if exists: if exists:
update_count += 1 update_count += 1
action = 'update' action = 'update'
else: else:
create_count += 1 create_count += 1
action = 'create' action = 'create'
if len(preview_data) < 10: if len(preview_data) < 10:
preview_data.append({ preview_data.append({
'action': action, 'action': action,
@ -1012,19 +1016,19 @@ 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')
tenant_id = request.POST.get('tenant') tenant_id = request.POST.get('tenant')
tenant = Tenant.objects.get(id=tenant_id) tenant = Tenant.objects.get(id=tenant_id)
mapping = {} mapping = {}
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)
count = 0 count = 0
errors = 0 errors = 0
@ -1033,13 +1037,16 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
try: try:
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)
errors += 1 errors += 1
continue continue
try: try:
voter = Voter.objects.get(tenant=tenant, voter_id=voter_id) voter = Voter.objects.get(tenant=tenant, voter_id=voter_id)
except Voter.DoesNotExist: except Voter.DoesNotExist:
@ -1085,7 +1092,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
row["Import Error"] = str(e) row["Import Error"] = str(e)
failed_rows.append(row) failed_rows.append(row)
errors += 1 errors += 1
if os.path.exists(file_path): if os.path.exists(file_path):
os.remove(file_path) os.remove(file_path)
self.message_user(request, f"Successfully imported {count} participations.") self.message_user(request, f"Successfully imported {count} participations.")
@ -1096,26 +1103,26 @@ 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():
csv_file = request.FILES['file'] csv_file = request.FILES['file']
tenant = form.cleaned_data['tenant'] tenant = form.cleaned_data['tenant']
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)
@ -1132,7 +1139,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_mapping.html", context) return render(request, "admin/import_mapping.html", context)
else: else:
form = EventParticipationImportForm() form = EventParticipationImportForm()
context = self.admin_site.each_context(request) context = self.admin_site.each_context(request)
context['form'] = form context['form'] = form
context['title'] = "Import Participations" context['title'] = "Import Participations"
@ -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
@ -1179,18 +1186,18 @@ class DonationAdmin(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'))
exists = False exists = False
if voter_id: if voter_id:
exists = Voter.objects.filter(tenant=tenant, voter_id=voter_id).exists() exists = Voter.objects.filter(tenant=tenant, voter_id=voter_id).exists()
if exists: if exists:
update_count += 1 update_count += 1
action = 'update' action = 'update'
else: else:
create_count += 1 create_count += 1
action = 'create' action = 'create'
if len(preview_data) < 10: if len(preview_data) < 10:
preview_data.append({ preview_data.append({
'action': action, 'action': action,
@ -1213,13 +1220,13 @@ 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')
tenant_id = request.POST.get('tenant') tenant_id = request.POST.get('tenant')
tenant = Tenant.objects.get(id=tenant_id) tenant = Tenant.objects.get(id=tenant_id)
mapping = {} mapping = {}
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}')
@ -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)
@ -1248,7 +1258,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
failed_rows.append(row) failed_rows.append(row)
errors += 1 errors += 1
continue continue
try: try:
voter = Voter.objects.get(tenant=tenant, voter_id=voter_id) voter = Voter.objects.get(tenant=tenant, voter_id=voter_id)
except Voter.DoesNotExist: except Voter.DoesNotExist:
@ -1256,7 +1266,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
failed_rows.append(row) failed_rows.append(row)
errors += 1 errors += 1
continue continue
try: try:
if '/' in date_str: if '/' in date_str:
parsed_date = datetime.strptime(date_str, '%m/%d/%Y').date() parsed_date = datetime.strptime(date_str, '%m/%d/%Y').date()
@ -1280,7 +1290,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
failed_rows.append(row) failed_rows.append(row)
errors += 1 errors += 1
continue continue
donation_method, _ = DonationMethod.objects.get_or_create(tenant=tenant, name=method_name) donation_method, _ = DonationMethod.objects.get_or_create(tenant=tenant, name=method_name)
Donation.objects.create( Donation.objects.create(
@ -1295,7 +1305,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
row["Import Error"] = str(e) row["Import Error"] = str(e)
failed_rows.append(row) failed_rows.append(row)
errors += 1 errors += 1
if os.path.exists(file_path): if os.path.exists(file_path):
os.remove(file_path) os.remove(file_path)
self.message_user(request, f"Successfully imported {count} donations.") self.message_user(request, f"Successfully imported {count} donations.")
@ -1304,26 +1314,26 @@ 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():
csv_file = request.FILES['file'] csv_file = request.FILES['file']
tenant = form.cleaned_data['tenant'] tenant = form.cleaned_data['tenant']
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)
@ -1340,7 +1350,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_mapping.html", context) return render(request, "admin/import_mapping.html", context)
else: else:
form = DonationImportForm() form = DonationImportForm()
context = self.admin_site.each_context(request) context = self.admin_site.each_context(request)
context['form'] = form context['form'] = form
context['title'] = "Import Donations" context['title'] = "Import Donations"
@ -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
@ -1389,18 +1399,18 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
total_count += 1 total_count += 1
voter_id = row.get(mapping.get('voter_id')) voter_id = row.get(mapping.get('voter_id'))
volunteer_email = row.get(mapping.get('volunteer_email')) volunteer_email = row.get(mapping.get('volunteer_email'))
exists = False exists = False
if voter_id: if voter_id:
exists = Voter.objects.filter(tenant=tenant, voter_id=voter_id).exists() exists = Voter.objects.filter(tenant=tenant, voter_id=voter_id).exists()
if exists: if exists:
update_count += 1 update_count += 1
action = 'update' action = 'update'
else: else:
create_count += 1 create_count += 1
action = 'create' action = 'create'
if len(preview_data) < 10: if len(preview_data) < 10:
preview_data.append({ preview_data.append({
'action': action, 'action': action,
@ -1423,13 +1433,13 @@ 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')
tenant_id = request.POST.get('tenant') tenant_id = request.POST.get('tenant')
tenant = Tenant.objects.get(id=tenant_id) tenant = Tenant.objects.get(id=tenant_id)
mapping = {} mapping = {}
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}')
@ -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)
@ -1458,7 +1471,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
failed_rows.append(row) failed_rows.append(row)
errors += 1 errors += 1
continue continue
try: try:
voter = Voter.objects.get(tenant=tenant, voter_id=voter_id) voter = Voter.objects.get(tenant=tenant, voter_id=voter_id)
except Voter.DoesNotExist: except Voter.DoesNotExist:
@ -1466,7 +1479,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
failed_rows.append(row) failed_rows.append(row)
errors += 1 errors += 1
continue continue
volunteer = None volunteer = None
if volunteer_email: if volunteer_email:
try: try:
@ -1506,7 +1519,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
row["Import Error"] = str(e) row["Import Error"] = str(e)
failed_rows.append(row) failed_rows.append(row)
errors += 1 errors += 1
if os.path.exists(file_path): if os.path.exists(file_path):
os.remove(file_path) os.remove(file_path)
self.message_user(request, f"Successfully imported {count} interactions.") self.message_user(request, f"Successfully imported {count} interactions.")
@ -1515,26 +1528,26 @@ 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():
csv_file = request.FILES['file'] csv_file = request.FILES['file']
tenant = form.cleaned_data['tenant'] tenant = form.cleaned_data['tenant']
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)
@ -1551,7 +1564,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_mapping.html", context) return render(request, "admin/import_mapping.html", context)
else: else:
form = InteractionImportForm() form = InteractionImportForm()
context = self.admin_site.each_context(request) context = self.admin_site.each_context(request)
context['form'] = form context['form'] = form
context['title'] = "Import Interactions" context['title'] = "Import Interactions"
@ -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
@ -1598,18 +1611,18 @@ class VoterLikelihoodAdmin(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'))
exists = False exists = False
if voter_id: if voter_id:
exists = Voter.objects.filter(tenant=tenant, voter_id=voter_id).exists() exists = Voter.objects.filter(tenant=tenant, voter_id=voter_id).exists()
if exists: if exists:
update_count += 1 update_count += 1
action = 'update' action = 'update'
else: else:
create_count += 1 create_count += 1
action = 'create' action = 'create'
if len(preview_data) < 10: if len(preview_data) < 10:
preview_data.append({ preview_data.append({
'action': action, 'action': action,
@ -1632,13 +1645,13 @@ 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')
tenant_id = request.POST.get('tenant') tenant_id = request.POST.get('tenant')
tenant = Tenant.objects.get(id=tenant_id) tenant = Tenant.objects.get(id=tenant_id)
mapping = {} mapping = {}
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}')
@ -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,12 +1668,15 @@ 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)
errors += 1 errors += 1
continue continue
if not election_type_name or not likelihood_val: if not election_type_name or not likelihood_val:
row["Import Error"] = "Missing election type or likelihood" row["Import Error"] = "Missing election type or likelihood"
failed_rows.append(row) failed_rows.append(row)
@ -1674,7 +1690,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
failed_rows.append(row) failed_rows.append(row)
errors += 1 errors += 1
continue continue
election_type, _ = ElectionType.objects.get_or_create(tenant=tenant, name=election_type_name) election_type, _ = ElectionType.objects.get_or_create(tenant=tenant, name=election_type_name)
VoterLikelihood.objects.update_or_create( VoterLikelihood.objects.update_or_create(
@ -1688,7 +1704,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
row["Import Error"] = str(e) row["Import Error"] = str(e)
failed_rows.append(row) failed_rows.append(row)
errors += 1 errors += 1
if os.path.exists(file_path): if os.path.exists(file_path):
os.remove(file_path) os.remove(file_path)
self.message_user(request, f"Import complete: {count} likelihoods created/updated.") self.message_user(request, f"Import complete: {count} likelihoods created/updated.")
@ -1697,26 +1713,26 @@ 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():
csv_file = request.FILES['file'] csv_file = request.FILES['file']
tenant = form.cleaned_data['tenant'] tenant = form.cleaned_data['tenant']
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)
@ -1733,7 +1749,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_mapping.html", context) return render(request, "admin/import_mapping.html", context)
else: else:
form = VoterLikelihoodImportForm() form = VoterLikelihoodImportForm()
context = self.admin_site.each_context(request) context = self.admin_site.each_context(request)
context['form'] = form context['form'] = form
context['title'] = "Import Likelihoods" context['title'] = "Import Likelihoods"
@ -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
@ -1782,7 +1798,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
total_count += 1 total_count += 1
voter_id = row.get(mapping.get('voter_id')) voter_id = row.get(mapping.get('voter_id'))
election_date = row.get(mapping.get('election_date')) election_date = row.get(mapping.get('election_date'))
exists = False exists = False
if voter_id and election_date: if voter_id and election_date:
try: try:
@ -1797,18 +1813,18 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if dt: if dt:
exists = VotingRecord.objects.filter(voter__tenant=tenant, voter__voter_id=voter_id, election_date=dt).exists() exists = VotingRecord.objects.filter(voter__tenant=tenant, voter__voter_id=voter_id, election_date=dt).exists()
except ValueError: except ValueError:
# Handle cases where date parsing fails # Handle cases where date parsing fails
pass pass
if exists: if exists:
update_count += 1 update_count += 1
action = 'update' action = 'update'
else: else:
create_count += 1 create_count += 1
action = 'create' action = 'create'
if len(preview_data) < 10: if len(preview_data) < 10:
preview_data.append({ preview_data.append({
'action': action, 'action': action,
@ -1831,13 +1847,13 @@ 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')
tenant_id = request.POST.get('tenant') tenant_id = request.POST.get('tenant')
tenant = Tenant.objects.get(id=tenant_id) tenant = Tenant.objects.get(id=tenant_id)
mapping = {} mapping = {}
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}')
@ -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)
@ -1866,7 +1885,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
failed_rows.append(row) failed_rows.append(row)
errors += 1 errors += 1
continue continue
try: try:
voter = Voter.objects.get(tenant=tenant, voter_id=voter_id) voter = Voter.objects.get(tenant=tenant, voter_id=voter_id)
except Voter.DoesNotExist: except Voter.DoesNotExist:
@ -1905,7 +1924,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
row["Import Error"] = str(e) row["Import Error"] = str(e)
failed_rows.append(row) failed_rows.append(row)
errors += 1 errors += 1
if os.path.exists(file_path): if os.path.exists(file_path):
os.remove(file_path) os.remove(file_path)
self.message_user(request, f"Successfully imported {count} voting records.") self.message_user(request, f"Successfully imported {count} voting records.")
@ -1914,26 +1933,26 @@ 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():
csv_file = request.FILES['file'] csv_file = request.FILES['file']
tenant = form.cleaned_data['tenant'] tenant = form.cleaned_data['tenant']
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)
@ -1950,7 +1969,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_mapping.html", context) return render(request, "admin/import_mapping.html", context)
else: else:
form = VotingRecordImportForm() form = VotingRecordImportForm()
context = self.admin_site.each_context(request) context = self.admin_site.each_context(request)
context['form'] = form context['form'] = form
context['title'] = "Import Voting Records" context['title'] = "Import Voting Records"

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)