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
)
from .forms import (
VoterImportForm, EventImportForm, EventParticipationImportForm,
VoterImportForm, EventImportForm, EventParticipationImportForm,
DonationImportForm, InteractionImportForm, VoterLikelihoodImportForm,
VolunteerImportForm, VotingRecordImportForm
)
@ -126,7 +126,7 @@ class BaseImportAdminMixin:
failed_rows = request.session.get(session_key, [])
if not failed_rows:
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["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]
readonly_fields = ('address',)
change_list_template = "admin/voter_change_list.html"
def changelist_view(self, request, extra_context=None):
extra_context = extra_context or {}
from core.models import Tenant
@ -274,7 +274,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
file_path = request.POST.get("file_path")
tenant_id = request.POST.get("tenant")
tenant = Tenant.objects.get(id=tenant_id)
mapping = {}
for field_name, _ in VOTER_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f"map_{field_name}")
@ -294,18 +294,18 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
preview_rows.append(row)
v_id = row.get(mapping.get("voter_id"))
if v_id:
voter_ids_for_preview.append(v_id)
voter_ids_for_preview.append(v_id.strip())
else:
break
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
update_count = 0
for row in preview_rows:
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
else:
create_count += 1
@ -326,12 +326,12 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_preview.html", context)
except Exception as e:
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
return redirect("..\\n")
return redirect("../")
elif "_import" in request.POST:
file_path = request.POST.get("file_path")
tenant_id = request.POST.get("tenant")
tenant = Tenant.objects.get(id=tenant_id)
mapping = {}
for field_name, _ in VOTER_MAPPABLE_FIELDS:
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):
total_processed += 1
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:
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)
skipped_no_id += 1
errors += 1
@ -446,7 +451,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
created_count += 1
else:
updated_count += 1
# Special handling for interests - assuming a comma-separated list in CSV
if 'interests' in mapping and row.get(mapping['interests']):
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:
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)
return redirect("..\\n")
return redirect("../")
except Exception as e:
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
return redirect("..\\n")
return redirect("../")
else:
form = VoterImportForm(request.POST, request.FILES)
if form.is_valid():
csv_file = request.FILES['file']
tenant = form.cleaned_data['tenant']
if not csv_file.name.endswith('.csv'):
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:
for chunk in csv_file.chunks():
tmp.write(chunk)
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)
headers = next(reader)
@ -514,7 +519,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_mapping.html", context)
else:
form = VoterImportForm()
context = self.admin_site.each_context(request)
context['form'] = form
context['title'] = "Import Voters"
@ -554,7 +559,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
for field_name, _ in EVENT_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f'map_{field_name}')
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)
total_count = 0
create_count = 0
@ -564,7 +569,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
total_count += 1
event_name = row.get(mapping.get('name'))
event_date = row.get(mapping.get('date'))
exists = False
if event_name and event_date:
try:
@ -579,18 +584,18 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if dt:
exists = Event.objects.filter(tenant=tenant, name=event_name, date=dt).exists()
except ValueError:
# Handle cases where date parsing fails
pass
if exists:
update_count += 1
action = 'update'
else:
create_count += 1
action = 'create'
if len(preview_data) < 10:
preview_data.append({
'action': action,
@ -613,13 +618,13 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_preview.html", context)
except Exception as e:
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
return redirect("..\\n")
return redirect("../")
elif "_import" in request.POST:
file_path = request.POST.get('file_path')
tenant_id = request.POST.get('tenant')
tenant = Tenant.objects.get(id=tenant_id)
mapping = {}
for field_name, _ in EVENT_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f'map_{field_name}')
@ -628,7 +633,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
count = 0
errors = 0
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)
for row in reader:
try:
@ -658,7 +663,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
failed_rows.append(row)
errors += 1
continue
event_type_obj, _ = EventType.objects.get_or_create(tenant=tenant, name=event_type_name)
defaults = {
@ -699,7 +704,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
row["Import Error"] = str(e)
failed_rows.append(row)
errors += 1
if os.path.exists(file_path):
os.remove(file_path)
self.message_user(request, f"Successfully imported {count} events.")
@ -708,26 +713,26 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if errors > 0:
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)
return redirect("..\\n")
return redirect("../")
except Exception as e:
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
return redirect("..\\n")
return redirect("../")
else:
form = EventImportForm(request.POST, request.FILES)
if form.is_valid():
csv_file = request.FILES['file']
tenant = form.cleaned_data['tenant']
if not csv_file.name.endswith('.csv'):
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:
for chunk in csv_file.chunks():
tmp.write(chunk)
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)
headers = next(reader)
@ -744,7 +749,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_mapping.html", context)
else:
form = EventImportForm()
context = self.admin_site.each_context(request)
context['form'] = form
context['title'] = "Import Events"
@ -784,7 +789,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
mapping[field_name] = request.POST.get(f'map_{field_name}')
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)
total_count = 0
create_count = 0
@ -797,14 +802,14 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
exists = False
if email:
exists = Volunteer.objects.filter(tenant=tenant, email=email).exists()
if exists:
update_count += 1
action = 'update'
else:
create_count += 1
action = 'create'
if len(preview_data) < 10:
preview_data.append({
'action': action,
@ -827,13 +832,13 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_preview.html", context)
except Exception as e:
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
return redirect("..\\n")
return redirect("../")
elif "_import" in request.POST:
file_path = request.POST.get('file_path')
tenant_id = request.POST.get('tenant')
tenant = Tenant.objects.get(id=tenant_id)
mapping = {}
for field_name, _ in VOLUNTEER_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f'map_{field_name}')
@ -842,7 +847,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
count = 0
errors = 0
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)
for row in reader:
try:
@ -852,7 +857,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
failed_rows.append(row)
errors += 1
continue
defaults = {
'first_name': row.get(mapping.get('first_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)
failed_rows.append(row)
errors += 1
if os.path.exists(file_path):
os.remove(file_path)
self.message_user(request, f"Successfully imported {count} volunteers.")
@ -880,26 +885,26 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if errors > 0:
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)
return redirect("..\\n")
return redirect("../")
except Exception as e:
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
return redirect("..\\n")
return redirect("../")
else:
form = VolunteerImportForm(request.POST, request.FILES)
if form.is_valid():
csv_file = request.FILES['file']
tenant = form.cleaned_data['tenant']
if not csv_file.name.endswith('.csv'):
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:
for chunk in csv_file.chunks():
tmp.write(chunk)
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)
headers = next(reader)
@ -916,7 +921,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_mapping.html", context)
else:
form = VolunteerImportForm()
context = self.admin_site.each_context(request)
context['form'] = form
context['title'] = "Import Volunteers"
@ -935,7 +940,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
extra_context = extra_context or {}
from core.models import Tenant
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):
urls = super().get_urls()
@ -955,7 +960,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
for field_name, _ in EVENT_PARTICIPATION_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f'map_{field_name}')
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)
total_count = 0
create_count = 0
@ -964,8 +969,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
for row in reader:
total_count += 1
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
csv_first_name = row.get(mapping.get('first_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()
except Voter.DoesNotExist:
pass
if exists:
update_count += 1
action = 'update'
else:
create_count += 1
action = 'create'
if len(preview_data) < 10:
preview_data.append({
'action': action,
@ -1012,19 +1016,19 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_preview.html", context)
except Exception as e:
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
return redirect("..\\n")
return redirect("../")
elif "_import" in request.POST:
file_path = request.POST.get('file_path')
tenant_id = request.POST.get('tenant')
tenant = Tenant.objects.get(id=tenant_id)
mapping = {}
for field_name, _ in EVENT_PARTICIPATION_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f'map_{field_name}')
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)
count = 0
errors = 0
@ -1033,13 +1037,16 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
try:
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
if voter_id: # Only strip if voter_id is not None
voter_id = voter_id.strip()
if not voter_id:
row["Import Error"] = "Missing voter ID"
failed_rows.append(row)
errors += 1
continue
try:
voter = Voter.objects.get(tenant=tenant, voter_id=voter_id)
except Voter.DoesNotExist:
@ -1085,7 +1092,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
row["Import Error"] = str(e)
failed_rows.append(row)
errors += 1
if os.path.exists(file_path):
os.remove(file_path)
self.message_user(request, f"Successfully imported {count} participations.")
@ -1096,26 +1103,26 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if errors > 0:
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)
return redirect("..\\n")
return redirect("../")
except Exception as e:
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
return redirect("..\\n")
return redirect("../")
else:
form = EventParticipationImportForm(request.POST, request.FILES)
if form.is_valid():
csv_file = request.FILES['file']
tenant = form.cleaned_data['tenant']
if not csv_file.name.endswith('.csv'):
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:
for chunk in csv_file.chunks():
tmp.write(chunk)
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)
headers = next(reader)
@ -1132,7 +1139,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_mapping.html", context)
else:
form = EventParticipationImportForm()
context = self.admin_site.each_context(request)
context['form'] = form
context['title'] = "Import Participations"
@ -1170,7 +1177,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
for field_name, _ in DONATION_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f'map_{field_name}')
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)
total_count = 0
create_count = 0
@ -1179,18 +1186,18 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
for row in reader:
total_count += 1
voter_id = row.get(mapping.get('voter_id'))
exists = False
if voter_id:
exists = Voter.objects.filter(tenant=tenant, voter_id=voter_id).exists()
if exists:
update_count += 1
action = 'update'
else:
create_count += 1
action = 'create'
if len(preview_data) < 10:
preview_data.append({
'action': action,
@ -1213,13 +1220,13 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_preview.html", context)
except Exception as e:
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
return redirect("..\\n")
return redirect("../")
elif "_import" in request.POST:
file_path = request.POST.get('file_path')
tenant_id = request.POST.get('tenant')
tenant = Tenant.objects.get(id=tenant_id)
mapping = {}
for field_name, _ in DONATION_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f'map_{field_name}')
@ -1228,7 +1235,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
count = 0
errors = 0
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)
for row in reader:
try:
@ -1237,6 +1244,9 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
amount_str = row.get(mapping.get('amount'))
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:
row["Import Error"] = "Missing voter ID"
failed_rows.append(row)
@ -1248,7 +1258,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
failed_rows.append(row)
errors += 1
continue
try:
voter = Voter.objects.get(tenant=tenant, voter_id=voter_id)
except Voter.DoesNotExist:
@ -1256,7 +1266,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
failed_rows.append(row)
errors += 1
continue
try:
if '/' in date_str:
parsed_date = datetime.strptime(date_str, '%m/%d/%Y').date()
@ -1280,7 +1290,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
failed_rows.append(row)
errors += 1
continue
donation_method, _ = DonationMethod.objects.get_or_create(tenant=tenant, name=method_name)
Donation.objects.create(
@ -1295,7 +1305,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
row["Import Error"] = str(e)
failed_rows.append(row)
errors += 1
if os.path.exists(file_path):
os.remove(file_path)
self.message_user(request, f"Successfully imported {count} donations.")
@ -1304,26 +1314,26 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if errors > 0:
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)
return redirect("..\\n")
return redirect("../")
except Exception as e:
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
return redirect("..\\n")
return redirect("../")
else:
form = DonationImportForm(request.POST, request.FILES)
if form.is_valid():
csv_file = request.FILES['file']
tenant = form.cleaned_data['tenant']
if not csv_file.name.endswith('.csv'):
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:
for chunk in csv_file.chunks():
tmp.write(chunk)
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)
headers = next(reader)
@ -1340,7 +1350,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_mapping.html", context)
else:
form = DonationImportForm()
context = self.admin_site.each_context(request)
context['form'] = form
context['title'] = "Import Donations"
@ -1379,7 +1389,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
for field_name, _ in INTERACTION_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f'map_{field_name}')
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)
total_count = 0
create_count = 0
@ -1389,18 +1399,18 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
total_count += 1
voter_id = row.get(mapping.get('voter_id'))
volunteer_email = row.get(mapping.get('volunteer_email'))
exists = False
if voter_id:
exists = Voter.objects.filter(tenant=tenant, voter_id=voter_id).exists()
if exists:
update_count += 1
action = 'update'
else:
create_count += 1
action = 'create'
if len(preview_data) < 10:
preview_data.append({
'action': action,
@ -1423,13 +1433,13 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_preview.html", context)
except Exception as e:
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
return redirect("..\\n")
return redirect("../")
elif "_import" in request.POST:
file_path = request.POST.get('file_path')
tenant_id = request.POST.get('tenant')
tenant = Tenant.objects.get(id=tenant_id)
mapping = {}
for field_name, _ in INTERACTION_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f'map_{field_name}')
@ -1438,7 +1448,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
count = 0
errors = 0
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)
for row in reader:
try:
@ -1447,6 +1457,9 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
date_str = row.get(mapping.get('date'))
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:
row["Import Error"] = "Missing voter ID"
failed_rows.append(row)
@ -1458,7 +1471,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
failed_rows.append(row)
errors += 1
continue
try:
voter = Voter.objects.get(tenant=tenant, voter_id=voter_id)
except Voter.DoesNotExist:
@ -1466,7 +1479,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
failed_rows.append(row)
errors += 1
continue
volunteer = None
if volunteer_email:
try:
@ -1506,7 +1519,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
row["Import Error"] = str(e)
failed_rows.append(row)
errors += 1
if os.path.exists(file_path):
os.remove(file_path)
self.message_user(request, f"Successfully imported {count} interactions.")
@ -1515,26 +1528,26 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if errors > 0:
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)
return redirect("..\\n")
return redirect("../")
except Exception as e:
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
return redirect("..\\n")
return redirect("../")
else:
form = InteractionImportForm(request.POST, request.FILES)
if form.is_valid():
csv_file = request.FILES['file']
tenant = form.cleaned_data['tenant']
if not csv_file.name.endswith('.csv'):
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:
for chunk in csv_file.chunks():
tmp.write(chunk)
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)
headers = next(reader)
@ -1551,7 +1564,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_mapping.html", context)
else:
form = InteractionImportForm()
context = self.admin_site.each_context(request)
context['form'] = form
context['title'] = "Import Interactions"
@ -1589,7 +1602,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
for field_name, _ in VOTER_LIKELIHOOD_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f'map_{field_name}')
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)
total_count = 0
create_count = 0
@ -1598,18 +1611,18 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
for row in reader:
total_count += 1
voter_id = row.get(mapping.get('voter_id'))
exists = False
if voter_id:
exists = Voter.objects.filter(tenant=tenant, voter_id=voter_id).exists()
if exists:
update_count += 1
action = 'update'
else:
create_count += 1
action = 'create'
if len(preview_data) < 10:
preview_data.append({
'action': action,
@ -1632,13 +1645,13 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_preview.html", context)
except Exception as e:
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
return redirect("..\\n")
return redirect("../")
elif "_import" in request.POST:
file_path = request.POST.get('file_path')
tenant_id = request.POST.get('tenant')
tenant = Tenant.objects.get(id=tenant_id)
mapping = {}
for field_name, _ in VOTER_LIKELIHOOD_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f'map_{field_name}')
@ -1647,7 +1660,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
count = 0
errors = 0
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)
for row in reader:
try:
@ -1655,12 +1668,15 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
election_type_name = row.get(mapping.get('election_type'))
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:
row["Import Error"] = "Missing voter ID"
failed_rows.append(row)
errors += 1
continue
if not election_type_name or not likelihood_val:
row["Import Error"] = "Missing election type or likelihood"
failed_rows.append(row)
@ -1674,7 +1690,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
failed_rows.append(row)
errors += 1
continue
election_type, _ = ElectionType.objects.get_or_create(tenant=tenant, name=election_type_name)
VoterLikelihood.objects.update_or_create(
@ -1688,7 +1704,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
row["Import Error"] = str(e)
failed_rows.append(row)
errors += 1
if os.path.exists(file_path):
os.remove(file_path)
self.message_user(request, f"Import complete: {count} likelihoods created/updated.")
@ -1697,26 +1713,26 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if errors > 0:
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)
return redirect("..\\n")
return redirect("../")
except Exception as e:
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
return redirect("..\\n")
return redirect("../")
else:
form = VoterLikelihoodImportForm(request.POST, request.FILES)
if form.is_valid():
csv_file = request.FILES['file']
tenant = form.cleaned_data['tenant']
if not csv_file.name.endswith('.csv'):
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:
for chunk in csv_file.chunks():
tmp.write(chunk)
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)
headers = next(reader)
@ -1733,7 +1749,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_mapping.html", context)
else:
form = VoterLikelihoodImportForm()
context = self.admin_site.each_context(request)
context['form'] = form
context['title'] = "Import Likelihoods"
@ -1772,7 +1788,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
for field_name, _ in VOTING_RECORD_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f'map_{field_name}')
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)
total_count = 0
create_count = 0
@ -1782,7 +1798,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
total_count += 1
voter_id = row.get(mapping.get('voter_id'))
election_date = row.get(mapping.get('election_date'))
exists = False
if voter_id and election_date:
try:
@ -1797,18 +1813,18 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if dt:
exists = VotingRecord.objects.filter(voter__tenant=tenant, voter__voter_id=voter_id, election_date=dt).exists()
except ValueError:
# Handle cases where date parsing fails
pass
if exists:
update_count += 1
action = 'update'
else:
create_count += 1
action = 'create'
if len(preview_data) < 10:
preview_data.append({
'action': action,
@ -1831,13 +1847,13 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_preview.html", context)
except Exception as e:
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
return redirect("..\\n")
return redirect("../")
elif "_import" in request.POST:
file_path = request.POST.get('file_path')
tenant_id = request.POST.get('tenant')
tenant = Tenant.objects.get(id=tenant_id)
mapping = {}
for field_name, _ in VOTING_RECORD_MAPPABLE_FIELDS:
mapping[field_name] = request.POST.get(f'map_{field_name}')
@ -1846,7 +1862,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
count = 0
errors = 0
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)
for row in reader:
try:
@ -1855,6 +1871,9 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
election_description = row.get(mapping.get('election_description'))
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:
row["Import Error"] = "Missing voter ID"
failed_rows.append(row)
@ -1866,7 +1885,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
failed_rows.append(row)
errors += 1
continue
try:
voter = Voter.objects.get(tenant=tenant, voter_id=voter_id)
except Voter.DoesNotExist:
@ -1905,7 +1924,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
row["Import Error"] = str(e)
failed_rows.append(row)
errors += 1
if os.path.exists(file_path):
os.remove(file_path)
self.message_user(request, f"Successfully imported {count} voting records.")
@ -1914,26 +1933,26 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if errors > 0:
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)
return redirect("..\\n")
return redirect("../")
except Exception as e:
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
return redirect("..\\n")
return redirect("../")
else:
form = VotingRecordImportForm(request.POST, request.FILES)
if form.is_valid():
csv_file = request.FILES['file']
tenant = form.cleaned_data['tenant']
if not csv_file.name.endswith('.csv'):
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:
for chunk in csv_file.chunks():
tmp.write(chunk)
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)
headers = next(reader)
@ -1950,7 +1969,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
return render(request, "admin/import_mapping.html", context)
else:
form = VotingRecordImportForm()
context = self.admin_site.each_context(request)
context['form'] = form
context['title'] = "Import Voting Records"

View File

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

View File

@ -125,7 +125,7 @@ class Interest(models.Model):
return self.name
class Voter(models.Model):
SUPPORT_CHOICES = [
CANDIDATE_SUPPORT_CHOICES = [
('unknown', 'Unknown'),
('supporting', 'Supporting'),
('not_supporting', 'Not Supporting'),
@ -170,7 +170,7 @@ class Voter(models.Model):
precinct = models.CharField(max_length=100, blank=True, db_index=True)
registration_date = models.DateField(null=True, blank=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)
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)

View File

@ -49,6 +49,10 @@
<label class="form-label small fw-bold text-muted">Precinct</label>
{{ form.precinct }}
</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">
<label class="form-label small fw-bold text-muted">Phone Type</label>
{{ form.phone_type }}

View File

@ -17,7 +17,7 @@ from django.contrib import messages
from django.core.paginator import Paginator
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 .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 zoneinfo
from django.utils import timezone
@ -157,7 +157,7 @@ def voter_list(request):
if query:
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:
parts = [p.strip() for p in query.split(",")]
@ -358,7 +358,6 @@ def edit_likelihood(request, likelihood_id):
if request.method == 'POST':
form = VoterLikelihoodForm(request.POST, instance=likelihood, tenant=tenant)
if form.is_valid():
# Check for conflict with another record of same 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():
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'):
voters = voters.filter(Q(address__icontains=data['address']) | Q(address_street__icontains=data['address']))
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'):
voters = voters.filter(birthdate__month=data['birth_month'])
if data.get('city'):
@ -498,6 +497,8 @@ def voter_advanced_search(request):
voters = voters.filter(district=data['district'])
if data.get('precinct'):
voters = voters.filter(precinct=data['precinct'])
if data.get('email'):
voters = voters.filter(email__icontains=data['email'])
if data.get('phone_type'):
voters = voters.filter(phone_type=data['phone_type'])
if data.get('is_targeted'):
@ -563,7 +564,7 @@ def export_voters_csv(request):
if data.get('address'):
voters = voters.filter(Q(address__icontains=data['address']) | Q(address_street__icontains=data['address']))
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'):
voters = voters.filter(birthdate__month=data['birth_month'])
if data.get('city'):
@ -574,6 +575,8 @@ def export_voters_csv(request):
voters = voters.filter(district=data['district'])
if data.get('precinct'):
voters = voters.filter(precinct=data['precinct'])
if data.get('email'):
voters = voters.filter(email__icontains=data['email'])
if data.get('phone_type'):
voters = voters.filter(phone_type=data['phone_type'])
if data.get('is_targeted'):
@ -842,7 +845,7 @@ def voter_search_json(request):
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
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:
parts = [p.strip() for p in query.split(",") ]
@ -925,6 +928,7 @@ def volunteer_add(request):
'form': form,
'tenant': tenant,
'selected_tenant': tenant,
'is_create': True,
}
return render(request, 'core/volunteer_detail.html', context)
@ -1993,21 +1997,5 @@ def profile(request):
if request.method == 'POST':
u_form = UserUpdateForm(request.POST, instance=request.user)
v_form = VolunteerProfileForm(request.POST, instance=volunteer) if volunteer else None
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)
# v_form = VolunteerProfileForm(request.POST, instance=volunteer) if volunteer else None # Removed VolunteerProfileForm
v_form = None # Set v_form to None after removal