From c5d42d341f6237d3c3fe87cbde00e1b4f8c3d6a5 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 1 Feb 2026 05:34:01 +0000 Subject: [PATCH] Autosave: 20260201-053401 --- core/__pycache__/admin.cpython-311.pyc | Bin 116384 -> 116791 bytes core/__pycache__/forms.cpython-311.pyc | Bin 29089 -> 29099 bytes core/__pycache__/urls.cpython-311.pyc | Bin 5776 -> 5895 bytes core/__pycache__/views.cpython-311.pyc | Bin 76648 -> 80868 bytes core/admin.py | 449 +++++++++----------- core/forms.py | 2 +- core/templates/core/door_visit_history.html | 150 +++++++ core/templates/core/door_visits.html | 285 +++++++++---- core/templates/core/index.html | 1 + core/templates/core/voter_detail.html | 5 + core/urls.py | 1 + core/views.py | 86 +++- 12 files changed, 640 insertions(+), 339 deletions(-) create mode 100644 core/templates/core/door_visit_history.html diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 598b79551f2d49b52075d4261d66c8d9ef398d82..79533be5a006940d0b628015ef7f6ed06dca2228 100644 GIT binary patch delta 32397 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 z

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 E0O{FYZ~y=R diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index be8d15c56abd597362908125f5843d5a9eccdf6e..1bb1da0b3a87929bcf454410288e23a58a427cd7 100644 GIT binary patch delta 141 zcmZ4Zm~r)EM&9MTyj%=GU~!=?^TI~n;0#9I$%z@l%zSJ3Czoev3kxtYq_SiQfRunh z3R8+8NM2HCjqoxS28Pu@3;|JsDI&oPnxc~*X5?~LaVC}I=_Y5SCTC9$&h%p{@>o1C glM!h2{47(x4bm6XZ7=ZKUF5gB!f&_van?RI01avRDeJV zQ;NXk#0+6c!8Jn5SQr>q12F_d38n}KGiZuTewdNVTI2x~oE(|yH+fCwUq-&ktFlb_ ZFBm#sPs;L8i{DM&BcR7M5P=#K};?NR!)#E*%_r*cJNY;T7>Qc28tXDKFcGO&8@wv$2bA`p{gCo!o6%j=s z=>{e#ei-A-+ih;b4 tD5huta!ek~G4cyUuP9nxa0t1;5_*v(^a@MphkQn$iUw{FEV2MbBLJ@suK54} delta 572 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

sJ|m~P7|^K8w3sDHG&p!x}XD`A?N{T3fX|O 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 len(row): continue + val = row[idx].strip() + if val == "" and not created: continue # Skip empty updates for existing records unless specifically desired? + # Type conversion and normalization if field_name == "is_targeted": - val = str(val).lower() in ["true", "1", "yes"] + val = val.lower() in ["true", "1", "yes"] elif field_name in ["birthdate", "registration_date"]: - 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 + except: continue + if parsed_date: val = parsed_date + else: continue elif field_name == "candidate_support": val_lower = val.lower() if val_lower in support_choices: val = val_lower @@ -432,42 +456,45 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): else: val = "none" elif field_name in ["phone_type", "secondary_phone_type"]: val_lower = val.lower() - if val_lower in phone_type_choices: - val = val_lower - elif val_lower in phone_type_reverse: - val = phone_type_reverse[val_lower] - else: - val = "cell" + if val_lower in phone_type_choices: val = val_lower + elif val_lower in phone_type_reverse: val = phone_type_reverse[val_lower] + else: val = "cell" - current_val = getattr(voter, field_name) - if current_val != val: + if getattr(voter, field_name) != val: setattr(voter, field_name, val) changed = True + record_updated_fields.add(field_name) - old_phone = voter.phone - voter.phone = format_phone_number(voter.phone) - if voter.phone != old_phone: - changed = True + # Optimization: Only perform transformations if related fields are mapped + if is_phone_related or created: + old_p = voter.phone + voter.phone = format_phone_number(voter.phone) + if voter.phone != old_p: + changed = True + record_updated_fields.add("phone") + + old_sp = voter.secondary_phone + voter.secondary_phone = format_phone_number(voter.secondary_phone) + if voter.secondary_phone != old_sp: + changed = True + record_updated_fields.add("secondary_phone") - old_secondary_phone = voter.secondary_phone - voter.secondary_phone = format_phone_number(voter.secondary_phone) - if voter.secondary_phone != old_secondary_phone: - changed = True - - if voter.longitude: + if (is_coords_related or created) and voter.longitude: try: new_lon = Decimal(str(voter.longitude)[:12]) if voter.longitude != new_lon: voter.longitude = new_lon changed = True - except: - pass + record_updated_fields.add("longitude") + except: pass - old_address = voter.address - parts = [voter.address_street, voter.city, voter.state, voter.zip_code] - voter.address = ", ".join([p for p in parts if p]) - if voter.address != old_address: - changed = True + if is_address_related or created: + old_addr = voter.address + parts = [voter.address_street, voter.city, voter.state, voter.zip_code] + voter.address = ", ".join([p for p in parts if p]) + if voter.address != old_addr: + changed = True + record_updated_fields.add("address") if not changed: skipped_no_change += 1 @@ -478,27 +505,28 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): created_count += 1 else: to_update.append(voter) + batch_updated_fields.update(record_updated_fields) 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 len(failed_rows) < 1000: + row_dict = dict(zip(headers, row)) + row_dict["Import Error"] = str(e) + failed_rows.append(row_dict) if to_create: - Voter.objects.bulk_create(to_create) + Voter.objects.bulk_create(to_create, batch_size=batch_size) if to_update: - Voter.objects.bulk_update(to_update, update_fields, batch_size=250) + Voter.objects.bulk_update(to_update, list(batch_updated_fields), batch_size=batch_size) - 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.") + print(f"DEBUG: Voter import progress: {total_processed} processed. {count} created/updated. Errors: {errors}") if os.path.exists(file_path): os.remove(file_path) - 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) + self.message_user(request, 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)") request.session[f"{self.model._meta.model_name}_import_errors"] = failed_rows request.session.modified = True @@ -515,14 +543,12 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): 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) + for chunk in csv_file.chunks(): tmp.write(chunk) file_path = tmp.name with open(file_path, "r", encoding="utf-8-sig") as f: @@ -711,7 +737,8 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): if os.path.exists(file_path): os.remove(file_path) self.message_user(request, f"Successfully imported {count} events.") - request.session[f"{self.model._meta.model_name}_import_errors"] = failed_rows + # Optimization: Limit error log size in session to avoid overflow + request.session[f"{self.model._meta.model_name}_import_errors"] = failed_rows[:1000] request.session.modified = True logger.info(f"Stored {len(failed_rows)} failed rows in session for {self.model._meta.model_name}") if errors > 0: @@ -876,7 +903,8 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin): if os.path.exists(file_path): os.remove(file_path) self.message_user(request, f"Successfully imported {count} volunteers.") - request.session[f"{self.model._meta.model_name}_import_errors"] = failed_rows + # Optimization: Limit error log size in session to avoid overflow + request.session[f"{self.model._meta.model_name}_import_errors"] = failed_rows[:1000] request.session.modified = True if errors > 0: error_url = reverse("admin:volunteer-download-errors") @@ -1076,7 +1104,8 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin): if os.path.exists(file_path): os.remove(file_path) self.message_user(request, f"Successfully imported {count} participations.") - request.session[f"{self.model._meta.model_name}_import_errors"] = failed_rows + # Optimization: Limit error log size in session to avoid overflow + request.session[f"{self.model._meta.model_name}_import_errors"] = failed_rows[:1000] request.session.modified = True logger.info(f"Stored {len(failed_rows)} failed rows in session for {self.model._meta.model_name}") if errors > 0: @@ -1266,7 +1295,8 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): if os.path.exists(file_path): os.remove(file_path) self.message_user(request, f"Successfully imported {count} donations.") - request.session[f"{self.model._meta.model_name}_import_errors"] = failed_rows + # Optimization: Limit error log size in session to avoid overflow + request.session[f"{self.model._meta.model_name}_import_errors"] = failed_rows[:1000] request.session.modified = True logger.info(f"Stored {len(failed_rows)} failed rows in session for {self.model._meta.model_name}") if errors > 0: @@ -1468,7 +1498,8 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): if os.path.exists(file_path): os.remove(file_path) self.message_user(request, f"Successfully imported {count} interactions.") - request.session[f"{self.model._meta.model_name}_import_errors"] = failed_rows + # Optimization: Limit error log size in session to avoid overflow + request.session[f"{self.model._meta.model_name}_import_errors"] = failed_rows[:1000] request.session.modified = True logger.info(f"Stored {len(failed_rows)} failed rows in session for {self.model._meta.model_name}") if errors > 0: @@ -1533,6 +1564,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin): ] return my_urls + urls + def import_likelihoods(self, request): if request.method == "POST": if "_preview" in request.POST: @@ -1543,7 +1575,6 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin): 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) @@ -1616,17 +1647,17 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin): skipped_no_id = 0 errors = 0 failed_rows = [] - batch_size = 500 + batch_size = 2000 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)} - with open(file_path, "r", encoding="utf-8-sig") as f: - reader = csv.DictReader(f) + raw_reader = csv.reader(f) + headers = next(raw_reader) + h_idx = {h: i for i, h in enumerate(headers)} + v_id_col = mapping.get("voter_id") et_col = mapping.get("election_type") l_col = mapping.get("likelihood") @@ -1634,135 +1665,97 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin): 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}") + v_idx = h_idx[v_id_col] + e_idx = h_idx[et_col] + l_idx = h_idx[l_col] total_processed = 0 - for chunk in self.chunk_reader(reader, batch_size): + for chunk in self.chunk_reader(raw_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 + voter_ids = [] + chunk_data = [] + for row in chunk: + if len(row) <= max(v_idx, e_idx, l_idx): continue + v_id = row[v_idx].strip() + et_name = row[e_idx].strip() + l_val = row[l_idx].strip() + if v_id and et_name and l_val: + voter_ids.append(v_id) + chunk_data.append((v_id, et_name, l_val, row)) + else: + skipped_no_id += 1 + 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 + et_names = [d[1] for d in chunk_data] 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") + ).only("id", "likelihood", "voter__voter_id", "election_type__name").select_related("voter", "election_type") } to_create = [] to_update = [] processed_in_batch = set() - for row in chunk: + for v_id, et_name, l_val, row in chunk_data: 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 + 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] + 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) + to_create.append(VoterLikelihood(voter=voter, election_type=election_type, likelihood=normalized_l)) created_count += 1 - else: + elif vl.likelihood != normalized_l: + vl.likelihood = normalized_l to_update.append(vl) updated_count += 1 + else: + skipped_no_change += 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) + if to_create: VoterLikelihood.objects.bulk_create(to_create, batch_size=batch_size) + if to_update: VoterLikelihood.objects.bulk_update(to_update, ["likelihood"], batch_size=batch_size) - 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.") + print(f"DEBUG: Likelihood import progress: {total_processed} processed. {count} created/updated.") 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. Download failed records"), level=messages.WARNING) + self.message_user(request, f"Import complete: {count} likelihoods created/updated. ({created_count} new, {updated_count} updated, {skipped_no_change} skipped, {errors} errors)") 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: @@ -1770,20 +1763,15 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin): 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) + 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", @@ -1797,7 +1785,6 @@ 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" @@ -1832,6 +1819,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin): ] return my_urls + urls + def import_voting_records(self, request): if request.method == "POST": if "_preview" in request.POST: @@ -1842,7 +1830,6 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin): try: 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) reader = csv.DictReader(f) @@ -1875,15 +1862,13 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin): e_date_raw = row.get(ed_col) e_desc = str(row.get(desc_col, '')).strip() - # Try to parse date for accurate comparison in preview e_date = None if e_date_raw: for fmt in ["%Y-%m-%d", "%m/%d/%Y", "%d/%m/%Y", "%Y/%m/%d"]: try: e_date = datetime.strptime(str(e_date_raw).strip(), fmt).date() break - except: - continue + except: continue action = "update" if (v_id, e_date, e_desc) in existing_records else "create" preview_data.append({ @@ -1921,13 +1906,14 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin): created_count = 0 updated_count = 0 skipped_no_change = 0 - skipped_no_id = 0 errors = 0 - failed_rows = [] - batch_size = 500 + batch_size = 2000 with open(file_path, "r", encoding="utf-8-sig") as f: - reader = csv.DictReader(f) + raw_reader = csv.reader(f) + headers = next(raw_reader) + h_idx = {h: i for i, h in enumerate(headers)} + v_id_col = mapping.get("voter_id") ed_col = mapping.get("election_date") desc_col = mapping.get("election_description") @@ -1936,23 +1922,23 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin): if not v_id_col or not ed_col or not desc_col: raise ValueError("Missing mapping for Voter ID, Election Date, or Description") - print(f"DEBUG: Starting voting record import. Tenant: {tenant.name}") + v_idx = h_idx[v_id_col] + ed_idx = h_idx[ed_col] + desc_idx = h_idx[desc_col] + p_idx = h_idx.get(party_col) total_processed = 0 - for chunk in self.chunk_reader(reader, batch_size): + for chunk in self.chunk_reader(raw_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)] - - # Fetch existing voters + voter_ids = [row[v_idx].strip() for row in chunk if len(row) > v_idx and row[v_idx].strip()] voters = {v.voter_id: v for v in Voter.objects.filter(tenant=tenant, voter_id__in=voter_ids).only("id", "voter_id")} - # Fetch existing records existing_records = { (vr.voter.voter_id, vr.election_date, vr.election_description): vr for vr in VotingRecord.objects.filter( voter__tenant=tenant, voter__voter_id__in=voter_ids - ).select_related("voter") + ).only("id", "election_date", "election_description", "voter__voter_id").select_related("voter") } to_create = [] @@ -1962,92 +1948,59 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin): for row in chunk: total_processed += 1 try: - raw_v_id = row.get(v_id_col) - raw_ed = row.get(ed_col) - raw_desc = row.get(desc_col) - party = str(row.get(party_col, '')).strip() if party_col else "" + if len(row) <= max(v_idx, ed_idx, desc_idx): continue + v_id = row[v_idx].strip() + raw_ed = row[ed_idx].strip() + desc = row[desc_idx].strip() + party = row[p_idx].strip() if p_idx is not None and len(row) > p_idx else "" - if not raw_v_id or not raw_ed or not raw_desc: - skipped_no_id += 1 - continue - - v_id = str(raw_v_id).strip() - desc = str(raw_desc).strip() + if not v_id or not raw_ed or not desc: continue - # Parse date - e_date = None - val = str(raw_ed).strip() - for fmt in ["%Y-%m-%d", "%m/%d/%Y", "%d/%m/%Y", "%Y/%m/%d"]: - try: - e_date = datetime.strptime(val, fmt).date() - break - except: - continue - - if not e_date: - row["Import Error"] = f"Invalid date format: {val}" - failed_rows.append(row) - errors += 1 - continue - - if (v_id, e_date, desc) in processed_in_batch: - continue - processed_in_batch.add((v_id, e_date, desc)) + if (v_id, raw_ed, desc) in processed_in_batch: continue + processed_in_batch.add((v_id, raw_ed, desc)) voter = voters.get(v_id) if not voter: - row["Import Error"] = f"Voter {v_id} not found" - failed_rows.append(row) + errors += 1 + continue + + e_date = None + for fmt in ["%Y-%m-%d", "%m/%d/%Y", "%d/%m/%Y", "%Y/%m/%d"]: + try: + e_date = datetime.strptime(raw_ed, fmt).date() + break + except: continue + + if not e_date: errors += 1 continue vr = existing_records.get((v_id, e_date, desc)) - created = False if not vr: - vr = VotingRecord(voter=voter, election_date=e_date, election_description=desc, primary_party=party) - created = True - - if not created and vr.primary_party == party: - skipped_no_change += 1 - continue - - vr.primary_party = party - - if created: - to_create.append(vr) + to_create.append(VotingRecord(voter=voter, election_date=e_date, election_description=desc, primary_party=party)) created_count += 1 - else: + elif vr.primary_party != party: + vr.primary_party = party to_update.append(vr) updated_count += 1 + else: + skipped_no_change += 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: - VotingRecord.objects.bulk_create(to_create) - if to_update: - VotingRecord.objects.bulk_update(to_update, ["primary_party"], batch_size=250) + if to_create: VotingRecord.objects.bulk_create(to_create, batch_size=batch_size) + if to_update: VotingRecord.objects.bulk_update(to_update, ["primary_party"], batch_size=batch_size) - print(f"DEBUG: Voting record import progress: {total_processed} processed. {count} created/updated. {skipped_no_change} skipped (no change). {skipped_no_id} skipped (no ID/Data). {errors} errors.") + print(f"DEBUG: Voting record import progress: {total_processed} processed. {count} created/updated.") if os.path.exists(file_path): os.remove(file_path) - success_msg = f"Import complete: {count} voting records 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:votingrecord-download-errors") - self.message_user(request, mark_safe(f"Failed to import {errors} rows. Download failed records"), level=messages.WARNING) + self.message_user(request, f"Import complete: {count} voting records created/updated. ({created_count} new, {updated_count} updated, {skipped_no_change} skipped, {errors} errors)") return redirect("..") except Exception as e: - print(f"DEBUG: Voting record import failed: {e}") self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) return redirect("..") else: @@ -2055,20 +2008,15 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin): 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) + 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 Voting Record Fields", @@ -2082,7 +2030,6 @@ 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" diff --git a/core/forms.py b/core/forms.py index d7377a1..4705bbf 100644 --- a/core/forms.py +++ b/core/forms.py @@ -317,7 +317,7 @@ class DoorVisitLogForm(forms.Form): ] outcome = forms.ChoiceField( choices=OUTCOME_CHOICES, - widget=forms.RadioSelect(attrs={"class": "form-check-input"}), + widget=forms.RadioSelect(attrs={"class": "btn-check"}), label="Outcome" ) notes = forms.CharField( diff --git a/core/templates/core/door_visit_history.html b/core/templates/core/door_visit_history.html new file mode 100644 index 0000000..859abe7 --- /dev/null +++ b/core/templates/core/door_visit_history.html @@ -0,0 +1,150 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +

+
+
+

Door Visit History

+

Review completed door-to-door visits and outcomes.

+
+ +
+ + +
+
+
Visited Households
+ + {{ history.paginator.count }} Households Visited + +
+
+ + + + + + + + + + + + {% for household in history %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
Household AddressVoters VisitedLast VisitOutcomeInteractions
+
{{ household.address_display }}
+ {% if household.neighborhood %} + + {{ household.neighborhood }} + + {% endif %} + {% if household.district %} + + District: {{ household.district }} + + {% endif %} +
+
+ {% for voter_name in household.voters_at_address %} + + {{ voter_name }} + + {% endfor %} +
+
+
{{ household.last_visit_date|date:"M d, Y" }}
+
{{ household.last_visit_date|date:"H:i" }}
+
+ + {{ household.last_outcome }} + + + + {{ household.interaction_count }} Visit{{ household.interaction_count|pluralize }} + +
+
+ +
+

No door visits logged yet.

+

Visit the Planned Visits page to start logging visits.

+
+
+ + {% if history.paginator.num_pages > 1 %} + + {% endif %} +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/door_visits.html b/core/templates/core/door_visits.html index f1d0bec..91fbd48 100644 --- a/core/templates/core/door_visits.html +++ b/core/templates/core/door_visits.html @@ -1,91 +1,119 @@ {% extends "base.html" %} +{% load static %} {% block content %} -
-
-

Door Visits

+
+
- {{ households.paginator.count }} Unvisited Households +

Door Visits

+

Manage and track your door-to-door campaign progress.

+
+
-
+ +
+
+
Filters
+
-
Filters
-
+
- - + +
- - + +
- - + +
-
- +
+ + Reset
-
-
-
Unvisited Households
+ +
+
+
Unvisited Targeted Households
+ + {{ households.paginator.count }} Households Found +
- - +
+ - - - - - + + + + + {% for household in households %} - - - - + + + + {% empty %} {% endfor %} @@ -94,41 +122,33 @@ {% if households.paginator.num_pages > 1 %} - + + +
Target VotersNeighborhoodAddressCity, StateActionActionHousehold AddressTargeted VotersNeighborhoodDistrict
- {% for voter in household.target_voters %} - - {{ voter.first_name }} {{ voter.last_name }} - {% if not forloop.last %}, {% endif %} - {% endfor %} - - {% if household.neighborhood %} - {{ household.neighborhood }} - {% else %} - None - {% endif %} - {{ household.address_street }}{{ household.city }}, {{ household.state }} - +
{{ household.address_street }}
+
{{ household.city }}, {{ household.state }} {{ household.zip_code }}
+
+
+ {% for voter in household.target_voters %} + + {{ voter.first_name }} {{ voter.last_name }} + + {% endfor %} +
+
+ {% if household.neighborhood %} + + {{ household.neighborhood }} + + {% else %} + Not assigned + {% endif %} + + + {{ household.district|default:"-" }} + +
-
- -

No unvisited households found.

-

Try adjusting your filters or targeting more voters.

+
+
+

No unvisited households found.

+

Try adjusting your filters or target more voters.