Autosave: 20260128-212108
This commit is contained in:
parent
9c3c5219c6
commit
ae3d7f9f2e
Binary file not shown.
Binary file not shown.
Binary file not shown.
353
core/admin.py
353
core/admin.py
@ -308,14 +308,16 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
batch_size = 500
|
batch_size = 500
|
||||||
|
|
||||||
support_choices = dict(Voter.SUPPORT_CHOICES)
|
support_choices = dict(Voter.SUPPORT_CHOICES)
|
||||||
|
support_reverse = {v.lower(): k for k, v in support_choices.items()}
|
||||||
yard_sign_choices = dict(Voter.YARD_SIGN_CHOICES)
|
yard_sign_choices = dict(Voter.YARD_SIGN_CHOICES)
|
||||||
|
yard_sign_reverse = {v.lower(): k for k, v in yard_sign_choices.items()}
|
||||||
window_sticker_choices = dict(Voter.WINDOW_STICKER_CHOICES)
|
window_sticker_choices = dict(Voter.WINDOW_STICKER_CHOICES)
|
||||||
|
window_sticker_reverse = {v.lower(): k for k, v in window_sticker_choices.items()}
|
||||||
phone_type_choices = dict(Voter.PHONE_TYPE_CHOICES)
|
phone_type_choices = dict(Voter.PHONE_TYPE_CHOICES)
|
||||||
phone_type_reverse = {v.lower(): k for k, v in phone_type_choices.items()}
|
phone_type_reverse = {v.lower(): k for k, v in phone_type_choices.items()}
|
||||||
|
|
||||||
valid_fields = {f.name for f in Voter._meta.get_fields()}
|
valid_fields = {f.name for f in Voter._meta.get_fields()}
|
||||||
mapped_fields = {f for f in mapping.keys() if f in valid_fields}
|
mapped_fields = {f for f in mapping.keys() if f in valid_fields}
|
||||||
fetch_fields = list(mapped_fields | {"voter_id", "address", "phone", "longitude", "latitude"})
|
|
||||||
# Ensure derived/special fields are in update_fields
|
# Ensure derived/special fields are in update_fields
|
||||||
update_fields = list(mapped_fields | {"address", "phone", "longitude", "latitude"})
|
update_fields = list(mapped_fields | {"address", "phone", "longitude", "latitude"})
|
||||||
if "voter_id" in update_fields: update_fields.remove("voter_id")
|
if "voter_id" in update_fields: update_fields.remove("voter_id")
|
||||||
@ -342,7 +344,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
for chunk_index, chunk in enumerate(chunk_reader(reader, batch_size)):
|
for chunk_index, chunk in enumerate(chunk_reader(reader, batch_size)):
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
voter_ids = [str(row.get(v_id_col)).strip() for row in chunk if row.get(v_id_col)]
|
voter_ids = [str(row.get(v_id_col)).strip() for row in chunk if row.get(v_id_col)]
|
||||||
existing_voters = {v.voter_id: v for v in Voter.objects.filter(tenant=tenant, voter_id__in=voter_ids).only(*fetch_fields)}
|
existing_voters = {v.voter_id: v for v in Voter.objects.filter(tenant=tenant, voter_id__in=voter_ids)}
|
||||||
|
|
||||||
to_create = []
|
to_create = []
|
||||||
to_update = []
|
to_update = []
|
||||||
@ -397,14 +399,20 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
# If parsing fails, keep original or skip? Let's skip updating this field.
|
# If parsing fails, keep original or skip? Let's skip updating this field.
|
||||||
continue
|
continue
|
||||||
elif field_name == "candidate_support":
|
elif field_name == "candidate_support":
|
||||||
val = val.lower().replace(" ", "_")
|
val_lower = val.lower()
|
||||||
if val not in support_choices: val = "unknown"
|
if val_lower in support_choices: val = val_lower
|
||||||
|
elif val_lower in support_reverse: val = support_reverse[val_lower]
|
||||||
|
else: val = "unknown"
|
||||||
elif field_name == "yard_sign":
|
elif field_name == "yard_sign":
|
||||||
val = val.lower().replace(" ", "_")
|
val_lower = val.lower()
|
||||||
if val not in yard_sign_choices: val = "none"
|
if val_lower in yard_sign_choices: val = val_lower
|
||||||
|
elif val_lower in yard_sign_reverse: val = yard_sign_reverse[val_lower]
|
||||||
|
else: val = "none"
|
||||||
elif field_name == "window_sticker":
|
elif field_name == "window_sticker":
|
||||||
val = val.lower().replace(" ", "_")
|
val_lower = val.lower()
|
||||||
if val not in window_sticker_choices: val = "none"
|
if val_lower in window_sticker_choices: val = val_lower
|
||||||
|
elif val_lower in window_sticker_reverse: val = window_sticker_reverse[val_lower]
|
||||||
|
else: val = "none"
|
||||||
elif field_name == "phone_type":
|
elif field_name == "phone_type":
|
||||||
val_lower = val.lower()
|
val_lower = val.lower()
|
||||||
if val_lower in phone_type_choices:
|
if val_lower in phone_type_choices:
|
||||||
@ -518,6 +526,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
context["title"] = "Import Voters"
|
context["title"] = "Import Voters"
|
||||||
context["opts"] = self.model._meta
|
context["opts"] = self.model._meta
|
||||||
return render(request, "admin/import_csv.html", context)
|
return render(request, "admin/import_csv.html", context)
|
||||||
|
@admin.register(Event)
|
||||||
class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
||||||
list_display = ('id', 'name', 'event_type', 'date', 'start_time', 'end_time', 'tenant')
|
list_display = ('id', 'name', 'event_type', 'date', 'start_time', 'end_time', 'tenant')
|
||||||
list_filter = ('tenant', 'date', 'event_type')
|
list_filter = ('tenant', 'date', 'event_type')
|
||||||
@ -1505,49 +1514,63 @@ class VoterLikelihoodAdmin(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 = {k: request.POST.get(f"map_{k}") for k, _ in VOTER_LIKELIHOOD_MAPPABLE_FIELDS if request.POST.get(f"map_{k}")}
|
||||||
for field_name, _ in VOTER_LIKELIHOOD_MAPPABLE_FIELDS:
|
|
||||||
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:
|
||||||
|
# Fast count and partial preview
|
||||||
|
total_count = sum(1 for line in f) - 1
|
||||||
|
f.seek(0)
|
||||||
reader = csv.DictReader(f)
|
reader = csv.DictReader(f)
|
||||||
total_count = 0
|
preview_rows = []
|
||||||
create_count = 0
|
voter_ids_for_preview = set()
|
||||||
update_count = 0
|
election_types_for_preview = set()
|
||||||
preview_data = []
|
|
||||||
for row in reader:
|
v_id_col = mapping.get('voter_id')
|
||||||
total_count += 1
|
et_col = mapping.get('election_type')
|
||||||
voter_id = row.get(mapping.get('voter_id'))
|
|
||||||
election_type_name = row.get(mapping.get('election_type'))
|
if not v_id_col or not et_col:
|
||||||
exists = False
|
raise ValueError("Missing mapping for Voter ID or Election Type")
|
||||||
if voter_id and election_type_name:
|
|
||||||
exists = VoterLikelihood.objects.filter(voter__tenant=tenant, voter__voter_id=voter_id, election_type__name=election_type_name).exists()
|
for i, row in enumerate(reader):
|
||||||
|
if i < 10:
|
||||||
if exists:
|
preview_rows.append(row)
|
||||||
update_count += 1
|
v_id = row.get(v_id_col)
|
||||||
action = 'update'
|
et_name = row.get(et_col)
|
||||||
|
if v_id: voter_ids_for_preview.add(str(v_id).strip())
|
||||||
|
if et_name: election_types_for_preview.add(str(et_name).strip())
|
||||||
else:
|
else:
|
||||||
create_count += 1
|
break
|
||||||
action = 'create'
|
|
||||||
|
existing_likelihoods = set(VoterLikelihood.objects.filter(
|
||||||
if len(preview_data) < 10:
|
voter__tenant=tenant,
|
||||||
preview_data.append({
|
voter__voter_id__in=voter_ids_for_preview,
|
||||||
'action': action,
|
election_type__name__in=election_types_for_preview
|
||||||
'identifier': f"Voter: {voter_id}",
|
).values_list("voter__voter_id", "election_type__name"))
|
||||||
'details': f"Election: {election_type_name}, Likelihood: {row.get(mapping.get('likelihood', '')) or ''}"
|
|
||||||
})
|
preview_data = []
|
||||||
|
for row in preview_rows:
|
||||||
|
v_id = str(row.get(v_id_col, '')).strip()
|
||||||
|
et_name = str(row.get(et_col, '')).strip()
|
||||||
|
action = "update" if (v_id, et_name) in existing_likelihoods else "create"
|
||||||
|
preview_data.append({
|
||||||
|
"action": action,
|
||||||
|
"identifier": f"Voter: {v_id}, Election: {et_name}",
|
||||||
|
"details": f"Likelihood: {row.get(mapping.get('likelihood', '')) or ''}"
|
||||||
|
})
|
||||||
|
|
||||||
context = self.admin_site.each_context(request)
|
context = self.admin_site.each_context(request)
|
||||||
context.update({
|
context.update({
|
||||||
'title': "Import Preview",
|
"title": "Import Preview",
|
||||||
'total_count': total_count,
|
"total_count": total_count,
|
||||||
'create_count': create_count,
|
"create_count": "N/A",
|
||||||
'update_count': update_count,
|
"update_count": "N/A",
|
||||||
'preview_data': preview_data,
|
"preview_data": preview_data,
|
||||||
'mapping': mapping,
|
"mapping": mapping,
|
||||||
'file_path': file_path,
|
"file_path": file_path,
|
||||||
'tenant_id': tenant_id,
|
"tenant_id": tenant_id,
|
||||||
'action_url': request.path,
|
"action_url": request.path,
|
||||||
'opts': self.model._meta,
|
"opts": self.model._meta,
|
||||||
})
|
})
|
||||||
return render(request, "admin/import_preview.html", context)
|
return render(request, "admin/import_preview.html", context)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -1558,98 +1581,172 @@ class VoterLikelihoodAdmin(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 = {k: request.POST.get(f"map_{k}") for k, _ in VOTER_LIKELIHOOD_MAPPABLE_FIELDS if request.POST.get(f"map_{k}")}
|
||||||
mapping = {}
|
|
||||||
for field_name, _ in VOTER_LIKELIHOOD_MAPPABLE_FIELDS:
|
|
||||||
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(file_path, 'r', encoding='UTF-8') as f:
|
count = 0
|
||||||
reader = csv.DictReader(f)
|
created_count = 0
|
||||||
count = 0
|
updated_count = 0
|
||||||
errors = 0
|
skipped_no_change = 0
|
||||||
failed_rows = []
|
skipped_no_id = 0
|
||||||
for row in reader:
|
errors = 0
|
||||||
try:
|
failed_rows = []
|
||||||
voter_id = row.get(mapping.get('voter_id')) if mapping.get('voter_id') else None
|
batch_size = 500
|
||||||
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:
|
|
||||||
row["Import Error"] = f"Voter {voter_id} not found"
|
|
||||||
failed_rows.append(row)
|
|
||||||
errors += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
election_type_name = row.get(mapping.get('election_type'))
|
|
||||||
likelihood_val = row.get(mapping.get('likelihood'))
|
|
||||||
|
|
||||||
if not election_type_name or not likelihood_val:
|
|
||||||
row["Import Error"] = "Missing election type or likelihood value"
|
|
||||||
failed_rows.append(row)
|
|
||||||
errors += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
election_type, _ = ElectionType.objects.get_or_create(
|
|
||||||
tenant=tenant,
|
|
||||||
name=election_type_name
|
|
||||||
)
|
|
||||||
|
|
||||||
# Normalize likelihood
|
|
||||||
likelihood_choices = dict(VoterLikelihood.LIKELIHOOD_CHOICES)
|
|
||||||
normalized_likelihood = None
|
|
||||||
likelihood_val_lower = likelihood_val.lower().replace(' ', '_')
|
|
||||||
if likelihood_val_lower in likelihood_choices:
|
|
||||||
normalized_likelihood = likelihood_val_lower
|
|
||||||
else:
|
|
||||||
# Try to find by display name
|
|
||||||
for k, v in likelihood_choices.items():
|
|
||||||
if v.lower() == likelihood_val.lower():
|
|
||||||
normalized_likelihood = k
|
|
||||||
break
|
|
||||||
|
|
||||||
if not normalized_likelihood:
|
|
||||||
row["Import Error"] = f"Invalid likelihood value: {likelihood_val}"
|
|
||||||
failed_rows.append(row)
|
|
||||||
errors += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
defaults = {}
|
|
||||||
if normalized_likelihood and normalized_likelihood.strip():
|
|
||||||
defaults['likelihood'] = normalized_likelihood
|
|
||||||
|
|
||||||
VoterLikelihood.objects.update_or_create(
|
|
||||||
voter=voter,
|
|
||||||
election_type=election_type,
|
|
||||||
defaults=defaults
|
|
||||||
)
|
|
||||||
count += 1
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error importing: {e}")
|
|
||||||
row["Import Error"] = str(e)
|
|
||||||
failed_rows.append(row)
|
|
||||||
errors += 1
|
|
||||||
|
|
||||||
|
likelihood_choices = dict(VoterLikelihood.LIKELIHOOD_CHOICES)
|
||||||
|
likelihood_reverse = {v.lower(): k for k, v in likelihood_choices.items()}
|
||||||
|
|
||||||
|
# Pre-fetch election types for this tenant
|
||||||
|
election_types = {et.name: et for et in ElectionType.objects.filter(tenant=tenant)}
|
||||||
|
|
||||||
|
def chunk_reader(reader, size):
|
||||||
|
chunk = []
|
||||||
|
for row in reader:
|
||||||
|
chunk.append(row)
|
||||||
|
if len(chunk) == size:
|
||||||
|
yield chunk
|
||||||
|
chunk = []
|
||||||
|
if chunk:
|
||||||
|
yield chunk
|
||||||
|
|
||||||
|
with open(file_path, "r", encoding="utf-8-sig") as f:
|
||||||
|
reader = csv.DictReader(f)
|
||||||
|
v_id_col = mapping.get("voter_id")
|
||||||
|
et_col = mapping.get("election_type")
|
||||||
|
l_col = mapping.get("likelihood")
|
||||||
|
|
||||||
|
if not v_id_col or not et_col or not l_col:
|
||||||
|
raise ValueError("Missing mapping for Voter ID, Election Type, or Likelihood")
|
||||||
|
|
||||||
|
print(f"DEBUG: Starting likelihood import. Tenant: {tenant.name}")
|
||||||
|
|
||||||
|
total_processed = 0
|
||||||
|
for chunk in chunk_reader(reader, batch_size):
|
||||||
|
with transaction.atomic():
|
||||||
|
voter_ids = [str(row.get(v_id_col)).strip() for row in chunk if row.get(v_id_col)]
|
||||||
|
et_names = [str(row.get(et_col)).strip() for row in chunk if row.get(et_col)]
|
||||||
|
|
||||||
|
# Fetch existing voters
|
||||||
|
voters = {v.voter_id: v for v in Voter.objects.filter(tenant=tenant, voter_id__in=voter_ids).only("id", "voter_id")}
|
||||||
|
|
||||||
|
# Fetch existing likelihoods
|
||||||
|
existing_likelihoods = {
|
||||||
|
(vl.voter.voter_id, vl.election_type.name): vl
|
||||||
|
for vl in VoterLikelihood.objects.filter(
|
||||||
|
voter__tenant=tenant,
|
||||||
|
voter__voter_id__in=voter_ids,
|
||||||
|
election_type__name__in=et_names
|
||||||
|
).select_related("voter", "election_type")
|
||||||
|
}
|
||||||
|
|
||||||
|
to_create = []
|
||||||
|
to_update = []
|
||||||
|
processed_in_batch = set()
|
||||||
|
|
||||||
|
for row in chunk:
|
||||||
|
total_processed += 1
|
||||||
|
try:
|
||||||
|
raw_v_id = row.get(v_id_col)
|
||||||
|
raw_et_name = row.get(et_col)
|
||||||
|
raw_l_val = row.get(l_col)
|
||||||
|
|
||||||
|
if raw_v_id is None or raw_et_name is None or raw_l_val is None:
|
||||||
|
skipped_no_id += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
v_id = str(raw_v_id).strip()
|
||||||
|
et_name = str(raw_et_name).strip()
|
||||||
|
l_val = str(raw_l_val).strip()
|
||||||
|
|
||||||
|
if not v_id or not et_name or not l_val:
|
||||||
|
skipped_no_id += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (v_id, et_name) in processed_in_batch:
|
||||||
|
continue
|
||||||
|
processed_in_batch.add((v_id, et_name))
|
||||||
|
|
||||||
|
voter = voters.get(v_id)
|
||||||
|
if not voter:
|
||||||
|
print(f"DEBUG: Voter {v_id} not found for likelihood import")
|
||||||
|
row["Import Error"] = f"Voter {v_id} not found"
|
||||||
|
failed_rows.append(row)
|
||||||
|
errors += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get or create election type
|
||||||
|
if et_name not in election_types:
|
||||||
|
election_type, _ = ElectionType.objects.get_or_create(tenant=tenant, name=et_name)
|
||||||
|
election_types[et_name] = election_type
|
||||||
|
election_type = election_types[et_name]
|
||||||
|
|
||||||
|
# Normalize likelihood
|
||||||
|
normalized_l = None
|
||||||
|
l_val_lower = l_val.lower().replace(' ', '_')
|
||||||
|
if l_val_lower in likelihood_choices:
|
||||||
|
normalized_l = l_val_lower
|
||||||
|
elif l_val_lower in likelihood_reverse:
|
||||||
|
normalized_l = likelihood_reverse[l_val_lower]
|
||||||
|
else:
|
||||||
|
# Try to find by display name more broadly
|
||||||
|
for k, v in likelihood_choices.items():
|
||||||
|
if v.lower() == l_val.lower():
|
||||||
|
normalized_l = k
|
||||||
|
break
|
||||||
|
|
||||||
|
if not normalized_l:
|
||||||
|
row["Import Error"] = f"Invalid likelihood value: {l_val}"
|
||||||
|
failed_rows.append(row)
|
||||||
|
errors += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
vl = existing_likelihoods.get((v_id, et_name))
|
||||||
|
created = False
|
||||||
|
if not vl:
|
||||||
|
vl = VoterLikelihood(voter=voter, election_type=election_type, likelihood=normalized_l)
|
||||||
|
created = True
|
||||||
|
|
||||||
|
if not created and vl.likelihood == normalized_l:
|
||||||
|
skipped_no_change += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
vl.likelihood = normalized_l
|
||||||
|
|
||||||
|
if created:
|
||||||
|
to_create.append(vl)
|
||||||
|
created_count += 1
|
||||||
|
else:
|
||||||
|
to_update.append(vl)
|
||||||
|
updated_count += 1
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DEBUG: Error importing row {total_processed}: {e}")
|
||||||
|
row["Import Error"] = str(e)
|
||||||
|
failed_rows.append(row)
|
||||||
|
errors += 1
|
||||||
|
|
||||||
|
if to_create:
|
||||||
|
VoterLikelihood.objects.bulk_create(to_create)
|
||||||
|
if to_update:
|
||||||
|
VoterLikelihood.objects.bulk_update(to_update, ["likelihood"], batch_size=250)
|
||||||
|
|
||||||
|
print(f"DEBUG: Likelihood import progress: {total_processed} processed. {count} created/updated. {skipped_no_change} skipped (no change). {skipped_no_id} skipped (no ID). {errors} errors.")
|
||||||
|
|
||||||
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} likelihoods.")
|
|
||||||
|
success_msg = f"Import complete: {count} likelihoods created/updated. ({created_count} new, {updated_count} updated, {skipped_no_change} skipped with no changes, {skipped_no_id} skipped missing data, {errors} errors)"
|
||||||
|
self.message_user(success_msg)
|
||||||
|
|
||||||
request.session[f"{self.model._meta.model_name}_import_errors"] = failed_rows
|
request.session[f"{self.model._meta.model_name}_import_errors"] = failed_rows
|
||||||
request.session.modified = True
|
request.session.modified = True
|
||||||
logger.info(f"Stored {len(failed_rows)} failed rows in session for {self.model._meta.model_name}")
|
|
||||||
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("..")
|
return redirect("..")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
|
print(f"DEBUG: Likelihood import failed: {e}")
|
||||||
return redirect("..")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"DEBUG: Voter import failed: {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("..")
|
return redirect("..")
|
||||||
else:
|
else:
|
||||||
@ -1667,7 +1764,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
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)
|
||||||
|
|
||||||
@ -1694,4 +1791,4 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
@admin.register(CampaignSettings)
|
@admin.register(CampaignSettings)
|
||||||
class CampaignSettingsAdmin(admin.ModelAdmin):
|
class CampaignSettingsAdmin(admin.ModelAdmin):
|
||||||
list_display = ('tenant', 'donation_goal')
|
list_display = ('tenant', 'donation_goal')
|
||||||
list_filter = ('tenant',)
|
list_filter = ('tenant',)
|
||||||
|
|||||||
@ -501,15 +501,45 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer border-0 p-4 pt-0">
|
<div class="modal-footer border-0 p-4 pt-0 justify-content-between">
|
||||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteVoterModal">
|
||||||
<button type="submit" class="btn btn-primary px-4">Save Changes</button>
|
<i class="bi bi-trash me-1"></i>Delete Profile
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary px-4">Save Changes</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Delete Voter Confirmation Modal -->
|
||||||
|
<div class="modal fade" id="deleteVoterModal" tabindex="-1" aria-labelledby="deleteVoterModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content border-0">
|
||||||
|
<div class="modal-header border-0 bg-danger text-white">
|
||||||
|
<h5 class="modal-title" id="deleteVoterModalLabel">Delete Voter Profile</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body p-4 text-center">
|
||||||
|
<i class="bi bi-exclamation-triangle text-danger mb-3" style="font-size: 3rem;"></i>
|
||||||
|
<h4 class="mb-3">Are you sure?</h4>
|
||||||
|
<p class="text-muted">This action will permanently delete <strong>{% if voter.nickname %}{{ voter.nickname }}{% else %}{{ voter.first_name }}{% endif %} {{ voter.last_name }}</strong> and all associated data. This cannot be undone.</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer border-0 p-4 pt-0 justify-content-center">
|
||||||
|
<button type="button" class="btn btn-light px-4" data-bs-dismiss="modal">No, Keep Profile</button>
|
||||||
|
<form action="{% url 'voter_delete' voter.id %}" method="POST" class="ms-2">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-danger px-4">Yes, Delete Permanently</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Add Interaction Modal -->
|
<!-- Add Interaction Modal -->
|
||||||
<div class="modal fade" id="addInteractionModal" tabindex="-1" aria-hidden="true">
|
<div class="modal fade" id="addInteractionModal" tabindex="-1" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
@ -580,9 +610,14 @@
|
|||||||
<textarea name="notes" class="form-control" rows="2">{{ interaction.notes }}</textarea>
|
<textarea name="notes" class="form-control" rows="2">{{ interaction.notes }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer border-0 p-4 pt-0">
|
<div class="modal-footer border-0 p-4 pt-0 justify-content-between">
|
||||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteVoterModal">
|
||||||
<button type="submit" class="btn btn-primary px-4">Save Changes</button>
|
<i class="bi bi-trash me-1"></i>Delete Profile
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary px-4">Save Changes</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -680,9 +715,14 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer border-0 p-4 pt-0">
|
<div class="modal-footer border-0 p-4 pt-0 justify-content-between">
|
||||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteVoterModal">
|
||||||
<button type="submit" class="btn btn-primary px-4">Save Changes</button>
|
<i class="bi bi-trash me-1"></i>Delete Profile
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary px-4">Save Changes</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -770,9 +810,14 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer border-0 p-4 pt-0">
|
<div class="modal-footer border-0 p-4 pt-0 justify-content-between">
|
||||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteVoterModal">
|
||||||
<button type="submit" class="btn btn-primary px-4">Save Changes</button>
|
<i class="bi bi-trash me-1"></i>Delete Profile
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary px-4">Save Changes</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -860,9 +905,14 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer border-0 p-4 pt-0">
|
<div class="modal-footer border-0 p-4 pt-0 justify-content-between">
|
||||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteVoterModal">
|
||||||
<button type="submit" class="btn btn-primary px-4">Save Changes</button>
|
<i class="bi bi-trash me-1"></i>Delete Profile
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary px-4">Save Changes</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,6 +9,7 @@ urlpatterns = [
|
|||||||
path('voters/export-csv/', views.export_voters_csv, name='export_voters_csv'),
|
path('voters/export-csv/', views.export_voters_csv, name='export_voters_csv'),
|
||||||
path('voters/<int:voter_id>/', views.voter_detail, name='voter_detail'),
|
path('voters/<int:voter_id>/', views.voter_detail, name='voter_detail'),
|
||||||
path('voters/<int:voter_id>/edit/', views.voter_edit, name='voter_edit'),
|
path('voters/<int:voter_id>/edit/', views.voter_edit, name='voter_edit'),
|
||||||
|
path('voters/<int:voter_id>/delete/', views.voter_delete, name='voter_delete'),
|
||||||
path('voters/<int:voter_id>/geocode/', views.voter_geocode, name='voter_geocode'),
|
path('voters/<int:voter_id>/geocode/', views.voter_geocode, name='voter_geocode'),
|
||||||
|
|
||||||
path('voters/<int:voter_id>/interaction/add/', views.add_interaction, name='add_interaction'),
|
path('voters/<int:voter_id>/interaction/add/', views.add_interaction, name='add_interaction'),
|
||||||
@ -26,4 +27,4 @@ urlpatterns = [
|
|||||||
path('voters/<int:voter_id>/event-participation/add/', views.add_event_participation, name='add_event_participation'),
|
path('voters/<int:voter_id>/event-participation/add/', views.add_event_participation, name='add_event_participation'),
|
||||||
path('event-participation/<int:participation_id>/edit/', views.edit_event_participation, name='edit_event_participation'),
|
path('event-participation/<int:participation_id>/edit/', views.edit_event_participation, name='edit_event_participation'),
|
||||||
path('event-participation/<int:participation_id>/delete/', views.delete_event_participation, name='delete_event_participation'),
|
path('event-participation/<int:participation_id>/delete/', views.delete_event_participation, name='delete_event_participation'),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -539,4 +539,18 @@ def export_voters_csv(request):
|
|||||||
voter.get_candidate_support_display(), voter.get_yard_sign_display(), voter.get_window_sticker_display(), voter.notes
|
voter.get_candidate_support_display(), voter.get_yard_sign_display(), voter.get_window_sticker_display(), voter.notes
|
||||||
])
|
])
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
def voter_delete(request, voter_id):
|
||||||
|
"""
|
||||||
|
Delete a voter profile.
|
||||||
|
"""
|
||||||
|
selected_tenant_id = request.session.get('tenant_id')
|
||||||
|
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
||||||
|
voter = get_object_or_404(Voter, id=voter_id, tenant=tenant)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
voter.delete()
|
||||||
|
messages.success(request, "Voter profile deleted successfully.")
|
||||||
|
return redirect('voter_list')
|
||||||
|
|
||||||
|
return redirect('voter_detail', voter_id=voter.id)
|
||||||
|
|||||||
330
patch_voter_likelihood.py
Normal file
330
patch_voter_likelihood.py
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
file_path = 'core/admin.py'
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
start_line = -1
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if 'def import_likelihoods(self, request):' in line and 'class VoterLikelihoodAdmin' in lines[i-1]:
|
||||||
|
start_line = i
|
||||||
|
break
|
||||||
|
# Also check if it's just after search_fields or something
|
||||||
|
if 'def import_likelihoods(self, request):' in line:
|
||||||
|
# Check if we are inside VoterLikelihoodAdmin
|
||||||
|
# We can look back for @admin.register(VoterLikelihood)
|
||||||
|
j = i
|
||||||
|
while j > 0:
|
||||||
|
if '@admin.register(VoterLikelihood)' in lines[j]:
|
||||||
|
start_line = i
|
||||||
|
break
|
||||||
|
if '@admin.register' in lines[j] and j < i - 50: # Too far back
|
||||||
|
break
|
||||||
|
j -= 1
|
||||||
|
if start_line != -1:
|
||||||
|
break
|
||||||
|
|
||||||
|
if start_line == -1:
|
||||||
|
print("Could not find import_likelihoods in VoterLikelihoodAdmin")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Find the end of the method
|
||||||
|
# The method ends before @admin.register(CampaignSettings)
|
||||||
|
end_line = -1
|
||||||
|
for i in range(start_line, len(lines)):
|
||||||
|
if '@admin.register(CampaignSettings)' in lines[i]:
|
||||||
|
end_line = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if end_line == -1:
|
||||||
|
print("Could not find end of import_likelihoods")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
new_method = """ def import_likelihoods(self, request):
|
||||||
|
if request.method == "POST":
|
||||||
|
if "_preview" in request.POST:
|
||||||
|
file_path = request.POST.get('file_path')
|
||||||
|
tenant_id = request.POST.get('tenant')
|
||||||
|
tenant = Tenant.objects.get(id=tenant_id)
|
||||||
|
mapping = {k: request.POST.get(f"map_{k}") for k, _ in VOTER_LIKELIHOOD_MAPPABLE_FIELDS if request.POST.get(f"map_{k}")}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r', encoding='utf-8-sig') as f:
|
||||||
|
# Fast count and partial preview
|
||||||
|
total_count = sum(1 for line in f) - 1
|
||||||
|
f.seek(0)
|
||||||
|
reader = csv.DictReader(f)
|
||||||
|
preview_rows = []
|
||||||
|
voter_ids_for_preview = set()
|
||||||
|
election_types_for_preview = set()
|
||||||
|
|
||||||
|
v_id_col = mapping.get('voter_id')
|
||||||
|
et_col = mapping.get('election_type')
|
||||||
|
|
||||||
|
if not v_id_col or not et_col:
|
||||||
|
raise ValueError("Missing mapping for Voter ID or Election Type")
|
||||||
|
|
||||||
|
for i, row in enumerate(reader):
|
||||||
|
if i < 10:
|
||||||
|
preview_rows.append(row)
|
||||||
|
v_id = row.get(v_id_col)
|
||||||
|
et_name = row.get(et_col)
|
||||||
|
if v_id: voter_ids_for_preview.add(str(v_id).strip())
|
||||||
|
if et_name: election_types_for_preview.add(str(et_name).strip())
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
existing_likelihoods = set(VoterLikelihood.objects.filter(
|
||||||
|
voter__tenant=tenant,
|
||||||
|
voter__voter_id__in=voter_ids_for_preview,
|
||||||
|
election_type__name__in=election_types_for_preview
|
||||||
|
).values_list("voter__voter_id", "election_type__name"))
|
||||||
|
|
||||||
|
preview_data = []
|
||||||
|
for row in preview_rows:
|
||||||
|
v_id = str(row.get(v_id_col, '')).strip()
|
||||||
|
et_name = str(row.get(et_col, '')).strip()
|
||||||
|
action = "update" if (v_id, et_name) in existing_likelihoods else "create"
|
||||||
|
preview_data.append({
|
||||||
|
"action": action,
|
||||||
|
"identifier": f"Voter: {v_id}, Election: {et_name}",
|
||||||
|
"details": f"Likelihood: {row.get(mapping.get('likelihood', '')) or ''}"
|
||||||
|
})
|
||||||
|
|
||||||
|
context = self.admin_site.each_context(request)
|
||||||
|
context.update({
|
||||||
|
"title": "Import Preview",
|
||||||
|
"total_count": total_count,
|
||||||
|
"create_count": "N/A",
|
||||||
|
"update_count": "N/A",
|
||||||
|
"preview_data": preview_data,
|
||||||
|
"mapping": mapping,
|
||||||
|
"file_path": file_path,
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
"action_url": request.path,
|
||||||
|
"opts": self.model._meta,
|
||||||
|
})
|
||||||
|
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("..")
|
||||||
|
|
||||||
|
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 = {k: request.POST.get(f"map_{k}") for k, _ in VOTER_LIKELIHOOD_MAPPABLE_FIELDS if request.POST.get(f"map_{k}")}
|
||||||
|
|
||||||
|
try:
|
||||||
|
count = 0
|
||||||
|
created_count = 0
|
||||||
|
updated_count = 0
|
||||||
|
skipped_no_change = 0
|
||||||
|
skipped_no_id = 0
|
||||||
|
errors = 0
|
||||||
|
failed_rows = []
|
||||||
|
batch_size = 500
|
||||||
|
|
||||||
|
likelihood_choices = dict(VoterLikelihood.LIKELIHOOD_CHOICES)
|
||||||
|
likelihood_reverse = {v.lower(): k for k, v in likelihood_choices.items()}
|
||||||
|
|
||||||
|
# Pre-fetch election types for this tenant
|
||||||
|
election_types = {et.name: et for et in ElectionType.objects.filter(tenant=tenant)}
|
||||||
|
|
||||||
|
def chunk_reader(reader, size):
|
||||||
|
chunk = []
|
||||||
|
for row in reader:
|
||||||
|
chunk.append(row)
|
||||||
|
if len(chunk) == size:
|
||||||
|
yield chunk
|
||||||
|
chunk = []
|
||||||
|
if chunk:
|
||||||
|
yield chunk
|
||||||
|
|
||||||
|
with open(file_path, "r", encoding="utf-8-sig") as f:
|
||||||
|
reader = csv.DictReader(f)
|
||||||
|
v_id_col = mapping.get("voter_id")
|
||||||
|
et_col = mapping.get("election_type")
|
||||||
|
l_col = mapping.get("likelihood")
|
||||||
|
|
||||||
|
if not v_id_col or not et_col or not l_col:
|
||||||
|
raise ValueError("Missing mapping for Voter ID, Election Type, or Likelihood")
|
||||||
|
|
||||||
|
print(f"DEBUG: Starting likelihood import. Tenant: {tenant.name}")
|
||||||
|
|
||||||
|
total_processed = 0
|
||||||
|
for chunk in chunk_reader(reader, batch_size):
|
||||||
|
with transaction.atomic():
|
||||||
|
voter_ids = [str(row.get(v_id_col)).strip() for row in chunk if row.get(v_id_col)]
|
||||||
|
et_names = [str(row.get(et_col)).strip() for row in chunk if row.get(et_col)]
|
||||||
|
|
||||||
|
# Fetch existing voters
|
||||||
|
voters = {v.voter_id: v for v in Voter.objects.filter(tenant=tenant, voter_id__in=voter_ids).only("id", "voter_id")}
|
||||||
|
|
||||||
|
# Fetch existing likelihoods
|
||||||
|
existing_likelihoods = {
|
||||||
|
(vl.voter.voter_id, vl.election_type.name): vl
|
||||||
|
for vl in VoterLikelihood.objects.filter(
|
||||||
|
voter__tenant=tenant,
|
||||||
|
voter__voter_id__in=voter_ids,
|
||||||
|
election_type__name__in=et_names
|
||||||
|
).select_related("voter", "election_type")
|
||||||
|
}
|
||||||
|
|
||||||
|
to_create = []
|
||||||
|
to_update = []
|
||||||
|
processed_in_batch = set()
|
||||||
|
|
||||||
|
for row in chunk:
|
||||||
|
total_processed += 1
|
||||||
|
try:
|
||||||
|
raw_v_id = row.get(v_id_col)
|
||||||
|
raw_et_name = row.get(et_col)
|
||||||
|
raw_l_val = row.get(l_col)
|
||||||
|
|
||||||
|
if raw_v_id is None or raw_et_name is None or raw_l_val is None:
|
||||||
|
skipped_no_id += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
v_id = str(raw_v_id).strip()
|
||||||
|
et_name = str(raw_et_name).strip()
|
||||||
|
l_val = str(raw_l_val).strip()
|
||||||
|
|
||||||
|
if not v_id or not et_name or not l_val:
|
||||||
|
skipped_no_id += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (v_id, et_name) in processed_in_batch:
|
||||||
|
continue
|
||||||
|
processed_in_batch.add((v_id, et_name))
|
||||||
|
|
||||||
|
voter = voters.get(v_id)
|
||||||
|
if not voter:
|
||||||
|
print(f"DEBUG: Voter {v_id} not found for likelihood import")
|
||||||
|
row["Import Error"] = f"Voter {v_id} not found"
|
||||||
|
failed_rows.append(row)
|
||||||
|
errors += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get or create election type
|
||||||
|
if et_name not in election_types:
|
||||||
|
election_type, _ = ElectionType.objects.get_or_create(tenant=tenant, name=et_name)
|
||||||
|
election_types[et_name] = election_type
|
||||||
|
election_type = election_types[et_name]
|
||||||
|
|
||||||
|
# Normalize likelihood
|
||||||
|
normalized_l = None
|
||||||
|
l_val_lower = l_val.lower().replace(' ', '_')
|
||||||
|
if l_val_lower in likelihood_choices:
|
||||||
|
normalized_l = l_val_lower
|
||||||
|
elif l_val_lower in likelihood_reverse:
|
||||||
|
normalized_l = likelihood_reverse[l_val_lower]
|
||||||
|
else:
|
||||||
|
# Try to find by display name more broadly
|
||||||
|
for k, v in likelihood_choices.items():
|
||||||
|
if v.lower() == l_val.lower():
|
||||||
|
normalized_l = k
|
||||||
|
break
|
||||||
|
|
||||||
|
if not normalized_l:
|
||||||
|
row["Import Error"] = f"Invalid likelihood value: {l_val}"
|
||||||
|
failed_rows.append(row)
|
||||||
|
errors += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
vl = existing_likelihoods.get((v_id, et_name))
|
||||||
|
created = False
|
||||||
|
if not vl:
|
||||||
|
vl = VoterLikelihood(voter=voter, election_type=election_type, likelihood=normalized_l)
|
||||||
|
created = True
|
||||||
|
|
||||||
|
if not created and vl.likelihood == normalized_l:
|
||||||
|
skipped_no_change += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
vl.likelihood = normalized_l
|
||||||
|
|
||||||
|
if created:
|
||||||
|
to_create.append(vl)
|
||||||
|
created_count += 1
|
||||||
|
else:
|
||||||
|
to_update.append(vl)
|
||||||
|
updated_count += 1
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DEBUG: Error importing row {total_processed}: {e}")
|
||||||
|
row["Import Error"] = str(e)
|
||||||
|
failed_rows.append(row)
|
||||||
|
errors += 1
|
||||||
|
|
||||||
|
if to_create:
|
||||||
|
VoterLikelihood.objects.bulk_create(to_create)
|
||||||
|
if to_update:
|
||||||
|
VoterLikelihood.objects.bulk_update(to_update, ["likelihood"], batch_size=250)
|
||||||
|
|
||||||
|
print(f"DEBUG: Likelihood import progress: {total_processed} processed. {count} created/updated. {skipped_no_change} skipped (no change). {skipped_no_id} skipped (no ID). {errors} errors.")
|
||||||
|
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
os.remove(file_path)
|
||||||
|
|
||||||
|
success_msg = f"Import complete: {count} likelihoods created/updated. ({created_count} new, {updated_count} updated, {skipped_no_change} skipped with no changes, {skipped_no_id} skipped missing data, {errors} errors)"
|
||||||
|
self.message_user(request, success_msg)
|
||||||
|
|
||||||
|
request.session[f"{self.model._meta.model_name}_import_errors"] = failed_rows
|
||||||
|
request.session.modified = True
|
||||||
|
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("..")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DEBUG: Likelihood import failed: {e}")
|
||||||
|
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
|
||||||
|
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("..")
|
||||||
|
|
||||||
|
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-sig') as f:
|
||||||
|
reader = csv.reader(f)
|
||||||
|
headers = next(reader)
|
||||||
|
|
||||||
|
context = self.admin_site.each_context(request)
|
||||||
|
context.update({
|
||||||
|
'title': "Map Likelihood Fields",
|
||||||
|
'headers': headers,
|
||||||
|
'model_fields': VOTER_LIKELIHOOD_MAPPABLE_FIELDS,
|
||||||
|
'tenant_id': tenant.id,
|
||||||
|
'file_path': file_path,
|
||||||
|
'action_url': request.path,
|
||||||
|
'opts': self.model._meta,
|
||||||
|
})
|
||||||
|
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"
|
||||||
|
context['opts'] = self.model._meta
|
||||||
|
return render(request, "admin/import_csv.html", context)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
lines[start_line:end_line] = [new_method]
|
||||||
|
|
||||||
|
with open(file_path, 'w') as f:
|
||||||
|
f.writelines(lines)
|
||||||
|
|
||||||
|
print(f"Successfully patched {file_path}")
|
||||||
18
test_phone_type.py
Normal file
18
test_phone_type.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import os
|
||||||
|
import django
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from core.models import Voter
|
||||||
|
from core.forms import VoterForm
|
||||||
|
|
||||||
|
voter = Voter.objects.first()
|
||||||
|
if voter:
|
||||||
|
voter.phone_type = 'home'
|
||||||
|
voter.save()
|
||||||
|
form = VoterForm(instance=voter)
|
||||||
|
print(f"Voter ID: {voter.id}, Phone Type: {voter.phone_type}")
|
||||||
|
print(form['phone_type'].as_widget())
|
||||||
|
else:
|
||||||
|
print("No voters found.")
|
||||||
11
test_phone_type_choices.py
Normal file
11
test_phone_type_choices.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import os
|
||||||
|
import django
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from core.models import Voter
|
||||||
|
from core.forms import VoterForm
|
||||||
|
|
||||||
|
form = VoterForm()
|
||||||
|
print(f"Choices: {form.fields['phone_type'].choices}")
|
||||||
Loading…
x
Reference in New Issue
Block a user