From 9c3c5219c6917811851423005a837096021f8ed0 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 28 Jan 2026 13:41:32 +0000 Subject: [PATCH] .2 --- core/__pycache__/admin.cpython-311.pyc | Bin 90510 -> 94068 bytes core/admin.py | 117 +++++++++++++++++++------ 2 files changed, 92 insertions(+), 25 deletions(-) diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index b1885221482c5719671bac0e86f63295fc28fc64..7a8a9d77669f3c9cb39d50fcb13eb84fc46b7b66 100644 GIT binary patch delta 14394 zcmcI~34B}CneRQiTD&juzRR|}$d(sbw!GW%ns~|Tm?V(Ic6=4@+m-Aj&K0H|W++T1 z<8mHzhqA;l3{4?`!GKdrAq*{0Xp5W*VoVYeLLTiv!KR_`%gh_zcg~e$$#GIo-#q=K z?|kQ+?|f&y|NB4Z=-BIus}~djKlbJjf4t|#mS+P{nmCk$y#2?b8Rjl@SN&ZZ zMCNX+|6wTW$A2(WmR6}`z4%bPHSGjW&CZSF{H#&%i-une{9?NpYaEwkjmMMOQQ`+h zC=w-rsz+*)fIrNRM2S@M&x!O-;*#%Vgruot{Ky+gcxz5DO2+$hw)k)O!G;5fOS#ia zP)wyb^8z)NO{AUDN6_4x8�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 delta 12121 zcmb_i349aBx!=+1@NI)_`IIeN@*!J3_-wKC>JVHWK z!-Q^{qjB0C1V|ut8z5;CXxjdgwn-(T1Y%ADZIdQV?1sGZl9ayrb|qUfM`&O5lfL<8 z=9_Ql*!k~&zSY(9vWMT2g*+b|96*uJ2Y&rs2WPz;g3^>BBukUum#d>GY6sd;wxgV< zcH(NPjw~R?+(mN&n3W|CTQPctQ6F>H3CWK4gOtFz%@=zKc{aovZC`PxPvTHDUJ`KN| z+fZD#v&<#$-T%HHCZd6I1LxT#o`Tyq^OjY5{j~G?J2hANsI0b?9?ce z3n@fBP(}unNd}ZsLp?_<4oq0tQyD$JXfTwe9t?esI5|Lm;-@>)qj5& zemYwmHtmZE&H&$o^r|9zh(CqV6Vck8WxoBlXL6aWl*{BY$V8xBYA$0)kCWy^hG+WP zA&N`GyB~|1J*C{PXR_>BoZcF4Q+c{ZQ5^AjFx$85#4?OnX4$jJ3X01cG>BHM5N1hk z&%wXXPm9X&y{bKz%iWe@YsE+ND)mg{dSM<&H|KG3CdZxA+FHFXoGUuOFFu=q9omo< zCU-Ms&+U7YG%%16F7>BuZQka#6_YvN{<5Vu(_~$@!J~7z*!jE?ocl3(ViWxRZ{TX7 z1G#6i3cYILg_hU zRFIkycTMqaMjbj};0#;>87|Ekh{sTpOC&jeE|D|Dc*X(KB4JAHxwejpB8t;_N0^Wdf(~F7z1N^K88yo!l9u1|weC#Od26 zOuWsxJooh^;EmK2dm6KKWbrn7aunq=1H_KFjZ#pI@qS4Rxz~B+fIZ*4IhXHt?9Y+% zwh6aznLdSk*=YB&>AOok7qS-+FGf#}=w)7dxtCty(L1E}LZ)m8RpRQ;BW2|T&kFHi zdIIs&HsO`{nbZq>hMaAc3`%zWgv(-;Tsm$%p;U5u-)(`YzDQ07RT55*Pkb4hN*bKf zP_{fz7br?gu`PC`ZR=#o&OCiVB z+OVgT*w^_E*IvX?L$Rbo5yqX!w%5yWHWy7UGlwf8a_*Bd-)6R=Nz3l}I6A8c|KTSx z{6%@lv}IG4Y5~j>n#aF92ZiIbifA++"qetgma8NtMpjOYtJHNI0}FXoDe7U87F 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 diff --git a/core/admin.py b/core/admin.py index 4cf45bb..19ac800 100644 --- a/core/admin.py +++ b/core/admin.py @@ -240,7 +240,7 @@ class VoterAdmin(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: # Optimization: Fast count and partial preview total_count = sum(1 for line in f) - 1 f.seek(0) @@ -289,6 +289,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): 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") @@ -298,22 +299,25 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): try: count = 0 + created_count = 0 + updated_count = 0 + skipped_no_change = 0 + skipped_no_id = 0 errors = 0 failed_rows = [] - batch_size = 500 # Optimized batch size + batch_size = 500 - # Pre-calculate choice dicts and sets support_choices = dict(Voter.SUPPORT_CHOICES) yard_sign_choices = dict(Voter.YARD_SIGN_CHOICES) window_sticker_choices = dict(Voter.WINDOW_STICKER_CHOICES) phone_type_choices = dict(Voter.PHONE_TYPE_CHOICES) phone_type_reverse = {v.lower(): k for k, v in phone_type_choices.items()} - # Fields to fetch for change detection valid_fields = {f.name for f in Voter._meta.get_fields()} mapped_fields = {f for f in mapping.keys() if f in valid_fields} - fetch_fields = list(mapped_fields | {"voter_id", "address", "phone", "longitude"}) - update_fields = list(mapped_fields | {"address", "phone"}) + fetch_fields = list(mapped_fields | {"voter_id", "address", "phone", "longitude", "latitude"}) + # Ensure derived/special fields are in update_fields + update_fields = list(mapped_fields | {"address", "phone", "longitude", "latitude"}) if "voter_id" in update_fields: update_fields.remove("voter_id") def chunk_reader(reader, size): @@ -326,15 +330,18 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): if chunk: yield chunk - 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) v_id_col = mapping.get("voter_id") if not v_id_col: raise ValueError("Voter ID mapping is missing") + + print(f"DEBUG: Starting voter import. Tenant: {tenant.name}. Voter ID column: {v_id_col}") + total_processed = 0 for chunk_index, chunk in enumerate(chunk_reader(reader, batch_size)): with transaction.atomic(): - voter_ids = [row.get(v_id_col) 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)} to_create = [] @@ -342,9 +349,19 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): processed_in_batch = set() for row in chunk: + total_processed += 1 try: - voter_id = row.get(v_id_col) - if not voter_id or voter_id in processed_in_batch: + raw_voter_id = row.get(v_id_col) + if raw_voter_id is None: + skipped_no_id += 1 + continue + + voter_id = str(raw_voter_id).strip() + if not voter_id: + skipped_no_id += 1 + continue + + if voter_id in processed_in_batch: continue processed_in_batch.add(voter_id) @@ -359,25 +376,37 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): for field_name, csv_col in mapping.items(): if field_name == "voter_id": continue val = row.get(csv_col) - if val is None or str(val).strip() == "": continue + if val is None: continue + val = str(val).strip() + if val == "": continue - # Type-specific conversions if field_name == "is_targeted": val = str(val).lower() in ["true", "1", "yes"] elif field_name in ["birthdate", "registration_date"]: - try: - if isinstance(val, str): - val = datetime.strptime(val.strip(), "%Y-%m-%d").date() - except: - pass + orig_val = val + parsed_date = None + for fmt in ["%Y-%m-%d", "%m/%d/%Y", "%d/%m/%Y", "%Y/%m/%d"]: + try: + parsed_date = datetime.strptime(val, fmt).date() + break + except: + continue + if parsed_date: + val = parsed_date + else: + # If parsing fails, keep original or skip? Let's skip updating this field. + continue elif field_name == "candidate_support": + val = val.lower().replace(" ", "_") if val not in support_choices: val = "unknown" elif field_name == "yard_sign": + val = val.lower().replace(" ", "_") if val not in yard_sign_choices: val = "none" elif field_name == "window_sticker": + val = val.lower().replace(" ", "_") if val not in window_sticker_choices: val = "none" elif field_name == "phone_type": - val_lower = str(val).lower() + val_lower = val.lower() if val_lower in phone_type_choices: val = val_lower elif val_lower in phone_type_reverse: @@ -385,11 +414,11 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): else: val = "cell" - if getattr(voter, field_name) != val: + current_val = getattr(voter, field_name) + if current_val != val: setattr(voter, field_name, val) changed = True - # Special fields old_phone = voter.phone voter.phone = format_phone_number(voter.phone) if voter.phone != old_phone: @@ -411,16 +440,19 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): changed = True if not changed: + skipped_no_change += 1 continue if created: to_create.append(voter) + created_count += 1 else: to_update.append(voter) + updated_count += 1 count += 1 except Exception as e: - logger.error(f"Error importing row: {e}") + print(f"DEBUG: Error importing row {total_processed}: {e}") row["Import Error"] = str(e) failed_rows.append(row) errors += 1 @@ -430,11 +462,14 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): if to_update: Voter.objects.bulk_update(to_update, update_fields, batch_size=250) - logger.info(f"Voter import progress: Processed batch {chunk_index + 1}. Total successes: {count}") + print(f"DEBUG: Voter 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) - self.message_user(request, f"Successfully imported {count} voters.") + + success_msg = f"Import complete: {count} voters created/updated. ({created_count} new, {updated_count} updated, {skipped_no_change} skipped with no changes, {skipped_no_id} skipped missing ID, {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: @@ -442,7 +477,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): self.message_user(request, mark_safe(f"Failed to import {errors} rows. Download failed records"), level=messages.WARNING) return redirect("..") except Exception as e: - logger.exception("Voter import failed") + print(f"DEBUG: Voter import failed: {e}") self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) return redirect("..") else: @@ -460,7 +495,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): 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) @@ -475,6 +510,14 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): "opts": self.model._meta, }) 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" + context["opts"] = self.model._meta + return render(request, "admin/import_csv.html", context) class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): list_display = ('id', 'name', 'event_type', 'date', 'start_time', 'end_time', 'tenant') list_filter = ('tenant', 'date', 'event_type') @@ -626,6 +669,10 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) 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) + return redirect("..") else: form = EventImportForm(request.POST, request.FILES) if form.is_valid(): @@ -789,6 +836,10 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin): except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) 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) + return redirect("..") else: form = VolunteerImportForm(request.POST, request.FILES) if form.is_valid(): @@ -1001,6 +1052,10 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin): except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) 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) + return redirect("..") else: form = EventParticipationImportForm(request.POST, request.FILES) if form.is_valid(): @@ -1190,6 +1245,10 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) 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) + return redirect("..") else: form = DonationImportForm(request.POST, request.FILES) if form.is_valid(): @@ -1382,6 +1441,10 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) 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) + return redirect("..") else: form = InteractionImportForm(request.POST, request.FILES) if form.is_valid(): @@ -1585,6 +1648,10 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin): except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) 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) + return redirect("..") else: form = VoterLikelihoodImportForm(request.POST, request.FILES) if form.is_valid():