From d244ac9d3fa2a521e70099873389b5d49a8aa279 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Thu, 5 Feb 2026 19:09:04 +0000 Subject: [PATCH] Autosave: 20260205-190904 --- core/__pycache__/admin.cpython-311.pyc | Bin 116874 -> 105424 bytes core/__pycache__/forms.cpython-311.pyc | Bin 36046 -> 39292 bytes core/__pycache__/urls.cpython-311.pyc | Bin 6646 -> 7287 bytes core/__pycache__/views.cpython-311.pyc | Bin 95503 -> 113546 bytes core/admin.py | 1423 ++++++++--------- core/forms.py | 54 +- core/templates/admin/import_preview.html | 16 +- core/templates/core/event_detail.html | 30 +- .../core/event_participant_map_fields.html | 80 + .../core/event_participant_matching.html | 154 ++ core/templatetags/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 170 bytes .../__pycache__/core_tags.cpython-311.pyc | Bin 0 -> 525 bytes core/templatetags/core_tags.py | 7 + core/urls.py | 4 + core/views.py | 425 ++++- 16 files changed, 1394 insertions(+), 799 deletions(-) create mode 100644 core/templates/core/event_participant_map_fields.html create mode 100644 core/templates/core/event_participant_matching.html create mode 100644 core/templatetags/__init__.py create mode 100644 core/templatetags/__pycache__/__init__.cpython-311.pyc create mode 100644 core/templatetags/__pycache__/core_tags.cpython-311.pyc create mode 100644 core/templatetags/core_tags.py diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 709ba8107988806d606bddea3c5235dcbcaacfe0..d4f449480fc4cb31daa81eeb2d2bbdbb7e2f5868 100644 GIT binary patch literal 105424 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 z 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 D@1fFX diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index 58f6108ea1fcc41c269be79c5e3beabce7864eb7..33120a9c379623fb883e0b3a6bc1c96088aff790 100644 GIT binary patch delta 10904 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 S6CSO5qUjRomlHJdmH2RxRqA+evNzSA-#PyJ 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 OiSq9xVvJ8^|Na;Ea*baA diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index a0456dfdaef0b5ec314b520e81755b12c77a123a..d28eeba16bcbe54172312090659aad03428d2a43 100644 GIT binary patch delta 1536 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 X5um^vE-;4*Aa{;l`m|`^(f@w|#K~%1 diff --git a/core/admin.py b/core/admin.py index 2ecb813..e1d3803 100644 --- a/core/admin.py +++ b/core/admin.py @@ -75,6 +75,8 @@ EVENT_MAPPABLE_FIELDS = [ EVENT_PARTICIPATION_MAPPABLE_FIELDS = [ ('voter_id', 'Voter ID'), + ('first_name', 'First Name'), + ('last_name', 'Last Name'), ('event_name', 'Event Name'), ('participation_status', 'Participation Status'), ] @@ -124,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("..") + return redirect("..\\n") response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = f"attachment; filename={self.model._meta.model_name}_import_errors.csv" @@ -298,261 +300,204 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): existing_preview_ids = set(Voter.objects.filter(tenant=tenant, voter_id__in=voter_ids_for_preview).values_list("voter_id", flat=True)) - preview_data = [] + create_count = 0 + update_count = 0 + for row in preview_rows: - v_id = row.get(mapping.get("voter_id")) - action = "update" if v_id in existing_preview_ids else "create" - preview_data.append({ - "action": action, - "identifier": v_id, - "details": f"{row.get(mapping.get('first_name', '')) or ''} {row.get(mapping.get('last_name', '')) or ''}".strip() - }) - - 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) + voter_id_val = row.get(mapping.get("voter_id")) + if voter_id_val in existing_preview_ids: + update_count += 1 + else: + create_count += 1 + + context = self.admin_site.each_context(request) + context.update({ + "title": "Import Preview", + "total_count": total_count, + "create_count": create_count, + "update_count": update_count, + "preview_data": preview_rows, # This should be improved to show actual changes + "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("..") - + return redirect("..\\n") 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_MAPPABLE_FIELDS if request.POST.get(f"map_{k}")} + mapping = {} + for field_name, _ in VOTER_MAPPABLE_FIELDS: + mapping[field_name] = request.POST.get(f"map_{field_name}") try: - count = 0 created_count = 0 updated_count = 0 skipped_no_change = 0 skipped_no_id = 0 errors = 0 failed_rows = [] - batch_size = 2000 # Increased batch size - - # Pre-calculate choices and reverse mappings - 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_reverse = {v.lower(): k for k, v in yard_sign_choices.items()} - 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_reverse = {v.lower(): k for k, v in phone_type_choices.items()} - - # Identify what type of data is being imported to skip unnecessary logic - mapped_fields = set(mapping.keys()) - is_address_related = any(f in mapped_fields for f in ["address_street", "city", "state", "zip_code"]) - is_phone_related = any(f in mapped_fields for f in ["phone", "secondary_phone", "phone_type", "secondary_phone_type"]) - is_coords_related = any(f in mapped_fields for f in ["latitude", "longitude"]) + total_processed = 0 - with open(file_path, "r", encoding="utf-8-sig") as f: - # Optimization: Use csv.reader instead of DictReader for performance - raw_reader = csv.reader(f) - headers = next(raw_reader) - header_to_idx = {h: i for i, h in enumerate(headers)} - - v_id_col_name = mapping.get("voter_id") - if not v_id_col_name or v_id_col_name not in header_to_idx: - raise ValueError(f"Voter ID mapping '{v_id_col_name}' is missing or invalid") - - v_id_idx = header_to_idx[v_id_col_name] - - # Map internal field names to CSV column indices - mapping_indices = {k: header_to_idx[v] for k, v in mapping.items() if v in header_to_idx} - - # Optimization: Only fetch needed fields - fields_to_fetch = {"id", "voter_id"} | mapped_fields - if is_address_related: fields_to_fetch.add("address") - - print(f"DEBUG: Starting optimized voter import. Tenant: {tenant.name}. Fields: {mapped_fields}") + # Temporary storage for error rows to avoid holding large file in memory + temp_error_file = None + temp_error_file_path = None - total_processed = 0 - # Use chunk_reader with the raw_reader - for chunk in self.chunk_reader(raw_reader, batch_size): - with transaction.atomic(): - voter_ids = [] - chunk_data = [] - for row in chunk: - if len(row) <= v_id_idx: continue - v_id = row[v_id_idx].strip() - if v_id: - voter_ids.append(v_id) - chunk_data.append((v_id, row)) - else: - skipped_no_id += 1 - - # Fetch existing voters in one query - existing_voters = { - v.voter_id: v for v in Voter.objects.filter(tenant=tenant, voter_id__in=voter_ids) - .only(*fields_to_fetch) - } - - to_create = [] - to_update = [] - batch_updated_fields = set() - processed_in_batch = set() + # Process in chunks to reduce memory usage for very large files + with open(file_path, "r", encoding="utf-8-sig") as f_read: + reader = csv.DictReader(f_read) + for i, row in enumerate(reader): + total_processed += 1 + try: + voter_id = row.get(mapping.get("voter_id")) + if not voter_id: + row["Import Error"] = "Voter ID is required" + failed_rows.append(row) + skipped_no_id += 1 + errors += 1 + continue - for voter_id, row in chunk_data: - total_processed += 1 - try: - if voter_id in processed_in_batch: continue - processed_in_batch.add(voter_id) + defaults = {} + # Map other fields dynamically + for field_name, _ in VOTER_MAPPABLE_FIELDS: + csv_column = mapping.get(field_name) + if csv_column and csv_column in row: + field_value = row[csv_column].strip() + if field_name == "birthdate" or field_name == "registration_date": + # Handle date conversions + if field_value: + try: + # Attempt to parse common date formats + if '/' in field_value: + # Try MM/DD/YYYY or DD/MM/YYYY + if len(field_value.split('/')[2]) == 2: # YY format + dt = datetime.strptime(field_value, '%m/%d/%y').date() if len(field_value.split('/')[0]) < 3 else datetime.strptime(field_value, '%d/%m/%y').date() # noqa + else: + dt = datetime.strptime(field_value, '%m/%d/%Y').date() if len(field_value.split('/')[0]) < 3 else datetime.strptime(field_value, '%d/%m/%Y').date() # noqa + elif '-' in field_value: + # Try YYYY-MM-DD or DD-MM-YYYY or MM-DD-YYYY + if len(field_value.split('-')[0]) == 4: # YYYY format + dt = datetime.strptime(field_value, '%Y-%m-%d').date() + elif len(field_value.split('-')[2]) == 4: # YYYY format + dt = datetime.strptime(field_value, '%m-%d-%Y').date() if len(field_value.split('-')[0]) < 3 else datetime.strptime(field_value, '%d-%m-%Y').date() # noqa + else: + # Default to MM-DD-YY + dt = datetime.strptime(field_value, '%m-%d-%y').date() + else: + dt = None - voter = existing_voters.get(voter_id) - created = False - if not voter: - voter = Voter(tenant=tenant, voter_id=voter_id) - created = True - - changed = created - record_updated_fields = set() - - # Process mapped fields - for field_name, idx in mapping_indices.items(): - if field_name == "voter_id": continue - if idx >= 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 in ["is_targeted", "door_visit"]: - val = val.lower() in ["true", "1", "yes"] - elif field_name in ["birthdate", "registration_date"]: - 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: continue - elif field_name == "candidate_support": - val_lower = val.lower() - 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": - val_lower = val.lower() - 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": - val_lower = val.lower() - 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 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 getattr(voter, field_name) != val: - setattr(voter, field_name, val) - changed = True - record_updated_fields.add(field_name) - - # 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") - - 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 - record_updated_fields.add("longitude") - except: pass - - 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 - continue - - if created: - to_create.append(voter) - created_count += 1 + if dt: + defaults[field_name] = dt + else: + logger.warning(f"Could not parse date '{field_value}' for field {field_name}. Skipping.") + except ValueError as ve: + logger.warning(f"Date parsing error for '{field_value}' in field {field_name}: {ve}") + except Exception as ex: + logger.error(f"Unexpected error parsing date '{field_value}' for field {field_name}: {ex}") + elif field_name == "is_targeted" or field_name == "yard_sign" or field_name == "window_sticker" or field_name == "door_visit": + # Handle boolean fields + if field_value.lower() == 'true' or field_value == '1': + defaults[field_name] = True + elif field_value.lower() == 'false' or field_value == '0': + defaults[field_name] = False + else: + defaults[field_name] = None # Or sensible default/error + elif field_name == "phone": + defaults[field_name] = format_phone_number(field_value) + elif field_name == "email": + defaults[field_name] = field_value.lower() # Store emails as lowercase + elif field_name == "candidate_support": + if field_value in [choice[0] for choice in Voter.CANDIDATE_SUPPORT_CHOICES]: + defaults[field_name] = field_value + else: + logger.warning(f"Invalid candidate_support value: {field_value}. Skipping.") + elif field_name == "phone_type": + if field_value in [choice[0] for choice in Voter.PHONE_TYPE_CHOICES]: + defaults[field_name] = field_value + else: + logger.warning(f"Invalid phone_type value: {field_value}. Skipping.") + elif field_name == "secondary_phone_type": + if field_value in [choice[0] for choice in Voter.PHONE_TYPE_CHOICES]: + defaults[field_name] = field_value + else: + logger.warning(f"Invalid secondary_phone_type value: {field_value}. Skipping.") + elif field_name == "state" or field_name == "prior_state": + # Ensure state is uppercase and valid length + if field_value and len(field_value) <= 2: + defaults[field_name] = field_value.upper() + else: + logger.warning(f"Invalid state value: {field_value}. Skipping.") else: - to_update.append(voter) - batch_updated_fields.update(record_updated_fields) - updated_count += 1 - - count += 1 - except Exception as e: - errors += 1 - if len(failed_rows) < 1000: - row_dict = dict(zip(headers, row)) - row_dict["Import Error"] = str(e) - failed_rows.append(row_dict) + defaults[field_name] = field_value - if to_create: - Voter.objects.bulk_create(to_create, batch_size=batch_size) - if to_update: - 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. Errors: {errors}") + # Try to get voter. If not found, create new. Update if found. + voter, created = Voter.objects.update_or_create( + tenant=tenant, + voter_id=voter_id, + defaults=defaults + ) + if created: + 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()] + for interest_name in interest_names: + interest, _ = Interest.objects.get_or_create(tenant=tenant, name=interest_name) + voter.interests.add(interest) + if (i + 1) % 100 == 0: + print(f"DEBUG: Voter import progress: {total_processed} processed. {created_count} created. {updated_count} updated.") + + except Exception as e: + row["Import Error"] = str(e) + failed_rows.append(row) + errors += 1 + logger.error(f"Error importing row: {row}. Error: {e}") + + # Clean up the temporary file if os.path.exists(file_path): os.remove(file_path) - - 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 + + if temp_error_file_path and os.path.exists(temp_error_file_path): + os.remove(temp_error_file_path) + + self.message_user(request, f"Import complete: {created_count + updated_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)") + # Store failed rows in session for download, limit to avoid session 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:voter-download-errors") self.message_user(request, mark_safe(f"Failed to import {errors} rows. Download failed records"), level=messages.WARNING) - return redirect("..") + return redirect("..\\n") 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("..") + return redirect("..\\n") 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"): + 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("..") + return redirect("..\\n") - with tempfile.NamedTemporaryFile(delete=False, suffix=".csv") as tmp: - for chunk in csv_file.chunks(): tmp.write(chunk) + 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: + with open(file_path, 'r', encoding='UTF-8') as f: reader = csv.reader(f) headers = next(reader) @@ -569,23 +514,26 @@ 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" - context["opts"] = self.model._meta + context['form'] = form + context['title'] = "Import Voters" + context['opts'] = self.model._meta return render(request, "admin/import_csv.html", context) + + @admin.register(Event) class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): - list_display = ('id', 'name', 'event_type', 'date', 'location_name', 'city', 'state', 'tenant') - list_filter = ('tenant', 'date', 'event_type', 'city', 'state') - search_fields = ('name', 'description', 'location_name', 'address', 'city', 'state', 'zip_code') + list_display = ('name', 'date', 'event_type', 'tenant', 'location_name', 'address', 'city', 'state', 'zip_code') + list_filter = ('tenant', 'event_type') + search_fields = ('name', 'location_name', 'address', 'city', 'state', 'zip_code') + inlines = [VolunteerEventInline] change_list_template = "admin/event_change_list.html" - + def changelist_view(self, request, extra_context=None): extra_context = extra_context or {} from core.models import Tenant - extra_context["tenants"] = Tenant.objects.all() + extra_context['tenants'] = Tenant.objects.all() return super().changelist_view(request, extra_context=extra_context) def get_urls(self): @@ -614,15 +562,27 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): preview_data = [] for row in reader: total_count += 1 - date = row.get(mapping.get('date')) - event_type_name = row.get(mapping.get('event_type')) event_name = row.get(mapping.get('name')) + event_date = row.get(mapping.get('date')) + exists = False - if date and event_type_name: - q = Event.objects.filter(tenant=tenant, date=date, event_type__name=event_type_name) - if event_name: - q = q.filter(name=event_name) - exists = q.exists() + if event_name and event_date: + try: + # Assuming name and date uniquely identify an event + # This might need refinement based on actual data uniqueness requirements + if '/' in event_date: + dt = datetime.strptime(event_date, '%m/%d/%Y').date() + elif '-' in event_date: + dt = datetime.strptime(event_date, '%Y-%m-%d').date() + else: + dt = None + + 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 @@ -634,8 +594,8 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): if len(preview_data) < 10: preview_data.append({ 'action': action, - 'identifier': f"{event_name or 'No Name'} ({date} - {event_type_name})", - 'details': f"{row.get(mapping.get('city', '')) or ''}, {row.get(mapping.get('state', '')) or ''}" + 'identifier': f"Event: {event_name} (Date: {event_date})", + 'details': f"Location: {row.get(mapping.get('location_name', '')) or ''}" }) context = self.admin_site.each_context(request) context.update({ @@ -653,7 +613,7 @@ 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("..") + return redirect("..\\n") elif "_import" in request.POST: file_path = request.POST.get('file_path') @@ -665,67 +625,72 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): mapping[field_name] = request.POST.get(f'map_{field_name}') try: + count = 0 + errors = 0 + failed_rows = [] with open(file_path, 'r', encoding='UTF-8') as f: reader = csv.DictReader(f) - count = 0 - errors = 0 - failed_rows = [] for row in reader: try: - date = row.get(mapping.get('date')) if mapping.get('date') else None - event_type_name = row.get(mapping.get('event_type')) if mapping.get('event_type') else None - description = row.get(mapping.get('description')) if mapping.get('description') else None - location_name = row.get(mapping.get('location_name')) if mapping.get('location_name') else None - name = row.get(mapping.get('name')) if mapping.get('name') else None - start_time = row.get(mapping.get('start_time')) if mapping.get('start_time') else None - end_time = row.get(mapping.get('end_time')) if mapping.get('end_time') else None - address = row.get(mapping.get('address')) if mapping.get('address') else None - city = row.get(mapping.get('city')) if mapping.get('city') else None - state = row.get(mapping.get('state')) if mapping.get('state') else None - zip_code = row.get(mapping.get('zip_code')) if mapping.get('zip_code') else None - latitude = row.get(mapping.get('latitude')) if mapping.get('latitude') else None - longitude = row.get(mapping.get('longitude')) if mapping.get('longitude') else None + event_name = row.get(mapping.get('name')) + event_date = row.get(mapping.get('date')) + event_type_name = row.get(mapping.get('event_type')) - if not date or not event_type_name: - row["Import Error"] = "Missing date or event type" + if not event_name or not event_date or not event_type_name: + row["Import Error"] = "Missing event name, date, or type" + failed_rows.append(row) + errors += 1 + continue + + # Date parsing for event_date + try: + if '/' in event_date: + parsed_date = datetime.strptime(event_date, '%m/%d/%Y').date() + elif '-' in event_date: + parsed_date = datetime.strptime(event_date, '%Y-%m-%d').date() + else: + row["Import Error"] = "Invalid date format" + failed_rows.append(row) + errors += 1 + continue + except ValueError: + row["Import Error"] = "Invalid date format" failed_rows.append(row) errors += 1 continue - event_type, _ = EventType.objects.get_or_create( - tenant=tenant, - name=event_type_name - ) - - defaults = {} - if description and description.strip(): - defaults['description'] = description - if location_name and location_name.strip(): - defaults['location_name'] = location_name - if name and name.strip(): - defaults['name'] = name - if start_time and start_time.strip(): - defaults['start_time'] = start_time - if end_time and end_time.strip(): - defaults['end_time'] = end_time - if address and address.strip(): - defaults['address'] = address - if city and city.strip(): - defaults['city'] = city - if state and state.strip(): - defaults['state'] = state - if zip_code and zip_code.strip(): - defaults['zip_code'] = zip_code - if latitude and latitude.strip(): - defaults['latitude'] = latitude - if longitude and longitude.strip(): - defaults['longitude'] = longitude + event_type_obj, _ = EventType.objects.get_or_create(tenant=tenant, name=event_type_name) + + defaults = { + 'date': parsed_date, + 'event_type': event_type_obj, + 'description': row.get(mapping.get('description')) or '', + 'location_name': row.get(mapping.get('location_name')) or '', + 'address': row.get(mapping.get('address')) or '', + 'city': row.get(mapping.get('city')) or '', + 'state': row.get(mapping.get('state')) or '', + 'zip_code': row.get(mapping.get('zip_code')) or '', + 'latitude': row.get(mapping.get('latitude')) or None, + 'longitude': row.get(mapping.get('longitude')) or None, + } + + # Handle start_time and end_time + start_time_str = row.get(mapping.get('start_time')) + if start_time_str: + try: + defaults['start_time'] = datetime.strptime(start_time_str, '%H:%M').time() + except ValueError: + logger.warning(f"Invalid start_time format: {start_time_str}. Skipping.") + end_time_str = row.get(mapping.get('end_time')) + if end_time_str: + try: + defaults['end_time'] = datetime.strptime(end_time_str, '%H:%M').time() + except ValueError: + logger.warning(f"Invalid end_time format: {end_time_str}. Skipping.") - defaults['date'] = date - defaults['event_type'] = event_type Event.objects.update_or_create( tenant=tenant, - name=name or '', + name=event_name, defaults=defaults ) count += 1 @@ -738,17 +703,15 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): if os.path.exists(file_path): os.remove(file_path) self.message_user(request, f"Successfully imported {count} events.") - # 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: error_url = reverse("admin:event-download-errors") self.message_user(request, mark_safe(f"Failed to import {errors} rows. Download failed records"), level=messages.WARNING) - return redirect("..") + return redirect("..\\n") except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) - return redirect("..") + return redirect("..\\n") else: form = EventImportForm(request.POST, request.FILES) if form.is_valid(): @@ -757,7 +720,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): if not csv_file.name.endswith('.csv'): self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) - return redirect("..") + return redirect("..\\n") with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: for chunk in csv_file.chunks(): @@ -788,21 +751,18 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): context['opts'] = self.model._meta return render(request, "admin/import_csv.html", context) + @admin.register(Volunteer) class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin): - list_display = ('first_name', 'last_name', 'email', 'phone', 'tenant', 'user') - ordering = ("last_name", "first_name") + list_display = ('first_name', 'last_name', 'email', 'phone', 'tenant') list_filter = ('tenant',) - fields = ('tenant', 'user', 'first_name', 'last_name', 'email', 'phone', 'notes', 'interests') search_fields = ('first_name', 'last_name', 'email', 'phone') - inlines = [VolunteerEventInline] - filter_horizontal = ('interests',) change_list_template = "admin/volunteer_change_list.html" def changelist_view(self, request, extra_context=None): extra_context = extra_context or {} from core.models import Tenant - extra_context["tenants"] = Tenant.objects.all() + extra_context['tenants'] = Tenant.objects.all() return super().changelist_view(request, extra_context=extra_context) def get_urls(self): @@ -822,6 +782,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin): mapping = {} for field_name, _ in VOLUNTEER_MAPPABLE_FIELDS: mapping[field_name] = request.POST.get(f'map_{field_name}') + try: with open(file_path, 'r', encoding='UTF-8') as f: reader = csv.DictReader(f) @@ -832,18 +793,23 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin): for row in reader: total_count += 1 email = row.get(mapping.get('email')) - exists = Volunteer.objects.filter(tenant=tenant, email=email).exists() + + 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, - 'identifier': email, - 'details': f"{row.get(mapping.get('first_name', '')) or ''} {row.get(mapping.get('last_name', '')) or ''}".strip() + 'identifier': f"Volunteer: {email}", + 'details': f"Name: {row.get(mapping.get('first_name', '')) or ''} {row.get(mapping.get('last_name', '')) or ''}" }) context = self.admin_site.each_context(request) context.update({ @@ -861,21 +827,23 @@ 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("..") + return redirect("..\\n") 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}') + try: + count = 0 + errors = 0 + failed_rows = [] with open(file_path, 'r', encoding='UTF-8') as f: reader = csv.DictReader(f) - count = 0 - errors = 0 - failed_rows = [] for row in reader: try: email = row.get(mapping.get('email')) @@ -884,17 +852,18 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin): failed_rows.append(row) errors += 1 continue - volunteer_data = {} - for field_name, csv_col in mapping.items(): - if csv_col: - val = row.get(csv_col) - if val is not None and str(val).strip() != '': - if field_name == 'email': continue - volunteer_data[field_name] = val + + defaults = { + 'first_name': row.get(mapping.get('first_name')) or '', + 'last_name': row.get(mapping.get('last_name')) or '', + 'phone': format_phone_number(row.get(mapping.get('phone')) or ''), + 'notes': row.get(mapping.get('notes')) or '', + } + Volunteer.objects.update_or_create( tenant=tenant, email=email, - defaults=volunteer_data + defaults=defaults ) count += 1 except Exception as e: @@ -902,34 +871,38 @@ 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.") - # 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") self.message_user(request, mark_safe(f"Failed to import {errors} rows. Download failed records"), level=messages.WARNING) - return redirect("..") + return redirect("..\\n") except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) - return redirect("..") + return redirect("..\\n") 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("..") + return redirect("..\\n") + 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: reader = csv.reader(f) headers = next(reader) + context = self.admin_site.each_context(request) context.update({ 'title': "Map Volunteer Fields", @@ -943,24 +916,26 @@ 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" context['opts'] = self.model._meta return render(request, "admin/import_csv.html", context) -@admin.register(VolunteerEvent) -class VolunteerEventAdmin(admin.ModelAdmin): - list_display = ('volunteer', 'event', 'role_type') - list_filter = ('event__tenant', 'event', 'role_type') - autocomplete_fields = ["volunteer", "event"] @admin.register(EventParticipation) class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin): - list_display = ('voter', 'event', 'participation_status') - list_filter = ('event__tenant', 'event', 'participation_status') - autocomplete_fields = ["voter", "event"] - change_list_template = "admin/eventparticipation_change_list.html" + list_display = ('event', 'voter', 'participation_status') + list_filter = ('event', 'participation_status', 'voter__tenant') + search_fields = ('event__name', 'voter__first_name', 'voter__last_name', 'voter__voter_id') + change_list_template = 'admin/eventparticipation_change_list.html' + + def changelist_view(self, request, extra_context=None): + 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) def get_urls(self): urls = super().get_urls() @@ -991,10 +966,17 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin): 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'), '') + csv_full_name = f"{csv_first_name} {csv_last_name}".strip() + exists = False + voter_full_name = "N/A" # Initialize voter_full_name if voter_id: try: voter = Voter.objects.get(tenant=tenant, voter_id=voter_id) + voter_full_name = f"{voter.first_name} {voter.last_name}" # Get voter's full name if event_name: exists = EventParticipation.objects.filter(voter=voter, event__name=event_name).exists() except Voter.DoesNotExist: @@ -1010,7 +992,8 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin): if len(preview_data) < 10: preview_data.append({ 'action': action, - 'identifier': f"Voter: {voter_id}", + 'csv_full_name': csv_full_name, # Add CSV name + 'identifier': f"Voter: {voter_full_name} (ID: {voter_id})" if voter_id else "N/A", # Include full name 'details': f"Participation: {row.get(mapping.get('participation_status', '')) or ''}" }) context = self.admin_site.each_context(request) @@ -1029,7 +1012,7 @@ 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("..") + return redirect("..\\n") elif "_import" in request.POST: file_path = request.POST.get('file_path') @@ -1113,10 +1096,10 @@ 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. Download failed records"), level=messages.WARNING) - return redirect("..") + return redirect("..\\n") except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) - return redirect("..") + return redirect("..\\n") else: form = EventParticipationImportForm(request.POST, request.FILES) if form.is_valid(): @@ -1125,7 +1108,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin): if not csv_file.name.endswith('.csv'): self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) - return redirect("..") + return redirect("..\\n") with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: for chunk in csv_file.chunks(): @@ -1158,11 +1141,16 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin): @admin.register(Donation) class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): - list_display = ('id', 'voter', 'date', 'amount', 'method') - list_filter = ('voter__tenant', 'date', 'method') - search_fields = ('voter__first_name', 'voter__last_name', 'voter__voter_id') - autocomplete_fields = ["voter"] - change_list_template = "admin/donation_change_list.html" + list_display = ('voter', 'date', 'amount', 'method') + list_filter = ('voter__tenant', 'method') + search_fields = ('voter__first_name', 'voter__last_name', 'voter__voter_id', 'method__name') + change_list_template = 'admin/donation_change_list.html' + + def changelist_view(self, request, extra_context=None): + 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) def get_urls(self): urls = super().get_urls() @@ -1191,11 +1179,10 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): for row in reader: total_count += 1 voter_id = row.get(mapping.get('voter_id')) - date = row.get(mapping.get('date')) - amount = row.get(mapping.get('amount')) + exists = False - if voter_id and date and amount: - exists = Donation.objects.filter(voter__tenant=tenant, voter__voter_id=voter_id, date=date, amount=amount).exists() + if voter_id: + exists = Voter.objects.filter(tenant=tenant, voter_id=voter_id).exists() if exists: update_count += 1 @@ -1207,8 +1194,8 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): if len(preview_data) < 10: preview_data.append({ 'action': action, - 'identifier': f"Voter: {voter_id}", - 'details': f"Date: {date}, Amount: {amount}" + 'identifier': f"Voter ID: {voter_id}", + 'details': f"Amount: {row.get(mapping.get('amount', '')) or ''}, Method: {row.get(mapping.get('method', '')) or ''}" }) context = self.admin_site.each_context(request) context.update({ @@ -1226,7 +1213,7 @@ 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("..") + return redirect("..\\n") elif "_import" in request.POST: file_path = request.POST.get('file_path') @@ -1238,19 +1225,29 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): mapping[field_name] = request.POST.get(f'map_{field_name}') try: + count = 0 + errors = 0 + failed_rows = [] with open(file_path, 'r', encoding='UTF-8') as f: reader = csv.DictReader(f) - count = 0 - errors = 0 - failed_rows = [] for row in reader: try: - voter_id = row.get(mapping.get('voter_id')) if mapping.get('voter_id') else None + voter_id = row.get(mapping.get('voter_id')) + date_str = row.get(mapping.get('date')) + amount_str = row.get(mapping.get('amount')) + method_name = row.get(mapping.get('method')) + if not voter_id: row["Import Error"] = "Missing voter ID" failed_rows.append(row) errors += 1 continue + + if not date_str or not amount_str: + row["Import Error"] = "Missing date or amount" + failed_rows.append(row) + errors += 1 + continue try: voter = Voter.objects.get(tenant=tenant, voter_id=voter_id) @@ -1259,33 +1256,38 @@ 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() + elif '-' in date_str: + parsed_date = datetime.strptime(date_str, '%Y-%m-%d').date() + else: + row["Import Error"] = "Invalid date format" + failed_rows.append(row) + errors += 1 + continue + except ValueError: + row["Import Error"] = "Invalid date format" + failed_rows.append(row) + errors += 1 + continue - date = row.get(mapping.get('date')) - amount = row.get(mapping.get('amount')) - method_name = row.get(mapping.get('method')) - - if not date or not amount: - row["Import Error"] = "Missing date or amount" + try: + amount = Decimal(amount_str) + except InvalidOperation: + row["Import Error"] = "Invalid amount format" failed_rows.append(row) errors += 1 continue - method = None - if method_name and method_name.strip(): - method, _ = DonationMethod.objects.get_or_create( - tenant=tenant, - name=method_name - ) - - defaults = {} - if method: - defaults['method'] = method + donation_method, _ = DonationMethod.objects.get_or_create(tenant=tenant, name=method_name) - Donation.objects.update_or_create( + Donation.objects.create( voter=voter, - date=date, + date=parsed_date, amount=amount, - defaults=defaults + method=donation_method ) count += 1 except Exception as e: @@ -1297,17 +1299,15 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): if os.path.exists(file_path): os.remove(file_path) self.message_user(request, f"Successfully imported {count} donations.") - # 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: error_url = reverse("admin:donation-download-errors") self.message_user(request, mark_safe(f"Failed to import {errors} rows. Download failed records"), level=messages.WARNING) - return redirect("..") + return redirect("..\\n") except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) - return redirect("..") + return redirect("..\\n") else: form = DonationImportForm(request.POST, request.FILES) if form.is_valid(): @@ -1316,7 +1316,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): if not csv_file.name.endswith('.csv'): self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) - return redirect("..") + return redirect("..\\n") with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: for chunk in csv_file.chunks(): @@ -1349,11 +1349,17 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): @admin.register(Interaction) class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): - list_display = ('id', 'voter', 'volunteer', 'type', 'date', 'description') - list_filter = ('voter__tenant', 'type', 'date', 'volunteer') + list_display = ('voter', 'date', 'type', 'description', 'volunteer') + list_filter = ('voter__tenant', 'type', 'volunteer') search_fields = ('voter__first_name', 'voter__last_name', 'voter__voter_id', 'description', 'volunteer__first_name', 'volunteer__last_name') - autocomplete_fields = ["voter", "volunteer"] - change_list_template = "admin/interaction_change_list.html" + autocomplete_fields = ['voter', 'volunteer'] + change_list_template = 'admin/interaction_change_list.html' + + def changelist_view(self, request, extra_context=None): + 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) def get_urls(self): urls = super().get_urls() @@ -1382,10 +1388,11 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): for row in reader: total_count += 1 voter_id = row.get(mapping.get('voter_id')) - date = row.get(mapping.get('date')) + volunteer_email = row.get(mapping.get('volunteer_email')) + exists = False - if voter_id and date: - exists = Interaction.objects.filter(voter__tenant=tenant, voter__voter_id=voter_id, date=date).exists() + if voter_id: + exists = Voter.objects.filter(tenant=tenant, voter_id=voter_id).exists() if exists: update_count += 1 @@ -1397,8 +1404,8 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): if len(preview_data) < 10: preview_data.append({ 'action': action, - 'identifier': f"Voter: {voter_id}", - 'details': f"Date: {date}, Desc: {row.get(mapping.get('description', '')) or ''}" + 'identifier': f"Voter ID: {voter_id}", + 'details': f"Type: {row.get(mapping.get('type', '')) or ''}, Volunteer: {volunteer_email or ''}" }) context = self.admin_site.each_context(request) context.update({ @@ -1416,7 +1423,7 @@ 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("..") + return redirect("..\\n") elif "_import" in request.POST: file_path = request.POST.get('file_path') @@ -1428,19 +1435,29 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): mapping[field_name] = request.POST.get(f'map_{field_name}') try: + count = 0 + errors = 0 + failed_rows = [] with open(file_path, 'r', encoding='UTF-8') as f: reader = csv.DictReader(f) - count = 0 - errors = 0 - failed_rows = [] for row in reader: try: - voter_id = row.get(mapping.get('voter_id')) if mapping.get('voter_id') else None + voter_id = row.get(mapping.get('voter_id')) + volunteer_email = row.get(mapping.get('volunteer_email')) + date_str = row.get(mapping.get('date')) + type_name = row.get(mapping.get('type')) + if not voter_id: row["Import Error"] = "Missing voter ID" failed_rows.append(row) errors += 1 continue + + if not date_str or not type_name: + row["Import Error"] = "Missing date or description" + failed_rows.append(row) + errors += 1 + continue try: voter = Voter.objects.get(tenant=tenant, voter_id=voter_id) @@ -1449,46 +1466,39 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): failed_rows.append(row) errors += 1 continue + + volunteer = None + if volunteer_email: + try: + volunteer = Volunteer.objects.get(tenant=tenant, email=volunteer_email) + except Volunteer.DoesNotExist: + pass # Volunteer is optional - date = row.get(mapping.get('date')) - type_name = row.get(mapping.get('type')) - volunteer_email = row.get(mapping.get('volunteer_email')) - description = row.get(mapping.get('description')) - notes = row.get(mapping.get('notes')) - - if not date or not description: - row["Import Error"] = "Missing date or description" + try: + if '/' in date_str: + parsed_date = datetime.strptime(date_str, '%m/%d/%Y').date() + elif '-' in date_str: + parsed_date = datetime.strptime(date_str, '%Y-%m-%d').date() + else: + row["Import Error"] = "Invalid date format" + failed_rows.append(row) + errors += 1 + continue + except ValueError: + row["Import Error"] = "Invalid date format" failed_rows.append(row) errors += 1 continue - - volunteer = None - if volunteer_email and volunteer_email.strip(): - try: - volunteer = Volunteer.objects.get(tenant=tenant, email=volunteer_email.strip()) - except Volunteer.DoesNotExist: - pass - interaction_type = None - if type_name and type_name.strip(): - interaction_type, _ = InteractionType.objects.get_or_create( - tenant=tenant, - name=type_name - ) - - defaults = {} - if volunteer: - defaults['volunteer'] = volunteer - if interaction_type: - defaults['type'] = interaction_type - if description and description.strip(): - defaults['description'] = description - if notes and notes.strip(): - defaults['notes'] = notes - Interaction.objects.update_or_create( + interaction_type, _ = InteractionType.objects.get_or_create(tenant=tenant, name=type_name) + + Interaction.objects.create( voter=voter, - date=date, - defaults=defaults + volunteer=volunteer, + date=parsed_date, + type=interaction_type, + description=row.get(mapping.get('description')) or '', + notes=row.get(mapping.get('notes')) or '' ) count += 1 except Exception as e: @@ -1500,17 +1510,15 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): if os.path.exists(file_path): os.remove(file_path) self.message_user(request, f"Successfully imported {count} interactions.") - # 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: error_url = reverse("admin:interaction-download-errors") self.message_user(request, mark_safe(f"Failed to import {errors} rows. Download failed records"), level=messages.WARNING) - return redirect("..") + return redirect("..\\n") except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) - return redirect("..") + return redirect("..\\n") else: form = InteractionImportForm(request.POST, request.FILES) if form.is_valid(): @@ -1519,7 +1527,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): if not csv_file.name.endswith('.csv'): self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) - return redirect("..") + return redirect("..\\n") with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: for chunk in csv_file.chunks(): @@ -1552,11 +1560,16 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): @admin.register(VoterLikelihood) class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin): - list_display = ('id', 'voter', 'election_type', 'likelihood') + list_display = ('voter', 'election_type', 'likelihood') list_filter = ('voter__tenant', 'election_type', 'likelihood') - search_fields = ('voter__first_name', 'voter__last_name', 'voter__voter_id') - autocomplete_fields = ["voter"] - change_list_template = "admin/voterlikelihood_change_list.html" + search_fields = ('voter__first_name', 'voter__last_name', 'voter__voter_id', 'election_type__name') + change_list_template = 'admin/voterlikelihood_change_list.html' + + def changelist_view(self, request, extra_context=None): + 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) def get_urls(self): urls = super().get_urls() @@ -1566,214 +1579,147 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin): ] return my_urls + urls - 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}")} - + mapping = {} + 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-sig') as f: - total_count = sum(1 for line in f) - 1 - f.seek(0) + with open(file_path, 'r', encoding='UTF-8') as f: 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")) - + total_count = 0 + create_count = 0 + update_count = 0 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 ''}" - }) - + 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, + 'identifier': f"Voter ID: {voter_id}", + '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, + 'title': "Import Preview", + 'total_count': total_count, + 'create_count': create_count, + 'update_count': update_count, + '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("..") + return redirect("..\\n") 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}")} + + mapping = {} + for field_name, _ in VOTER_LIKELIHOOD_MAPPABLE_FIELDS: + mapping[field_name] = request.POST.get(f'map_{field_name}') try: count = 0 - created_count = 0 - updated_count = 0 - skipped_no_change = 0 - skipped_no_id = 0 errors = 0 failed_rows = [] - batch_size = 2000 - - likelihood_choices = dict(VoterLikelihood.LIKELIHOOD_CHOICES) - likelihood_reverse = {v.lower(): k for k, v in likelihood_choices.items()} - election_types = {et.name: et for et in ElectionType.objects.filter(tenant=tenant)} + with open(file_path, 'r', encoding='UTF-8') as f: + reader = csv.DictReader(f) + for row in reader: + try: + voter_id = row.get(mapping.get('voter_id')) + election_type_name = row.get(mapping.get('election_type')) + likelihood_val = row.get(mapping.get('likelihood')) - with open(file_path, "r", encoding="utf-8-sig") as 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") - - if not v_id_col or not et_col or not l_col: - raise ValueError("Missing mapping for Voter ID, Election Type, or Likelihood") - - 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(raw_reader, batch_size): - with transaction.atomic(): - 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")} - 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 - ).only("id", "likelihood", "voter__voter_id", "election_type__name").select_related("voter", "election_type") - } + if not voter_id: + row["Import Error"] = "Missing voter ID" + failed_rows.append(row) + errors += 1 + continue - to_create = [] - to_update = [] - processed_in_batch = set() + if not election_type_name or not likelihood_val: + row["Import Error"] = "Missing election type or likelihood" + failed_rows.append(row) + errors += 1 + continue - for v_id, et_name, l_val, row in chunk_data: - total_processed += 1 - try: - 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: - errors += 1 - continue - - 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] - - 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: - for k, v in likelihood_choices.items(): - if v.lower() == l_val.lower(): - normalized_l = k - break - - if not normalized_l: - errors += 1 - continue - - vl = existing_likelihoods.get((v_id, et_name)) - if not vl: - to_create.append(VoterLikelihood(voter=voter, election_type=election_type, likelihood=normalized_l)) - created_count += 1 - 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: - errors += 1 - - 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.") + 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, _ = ElectionType.objects.get_or_create(tenant=tenant, name=election_type_name) + VoterLikelihood.objects.update_or_create( + voter=voter, + election_type=election_type, + defaults={'likelihood': likelihood_val} + ) + count += 1 + except Exception as e: + print(f"DEBUG: Likelihood import failed: {e}") + 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. ({created_count} new, {updated_count} updated, {skipped_no_change} skipped, {errors} errors)") - return redirect("..") + self.message_user(request, f"Import complete: {count} likelihoods created/updated.") + request.session[f"{self.model._meta.model_name}_import_errors"] = failed_rows[:1000] + 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) + return redirect("..\\n") except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) - return redirect("..") + return redirect("..\\n") 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("..") + return redirect("..\\n") + 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: + + with open(file_path, 'r', encoding='UTF-8') as f: reader = csv.reader(f) headers = next(reader) + context = self.admin_site.each_context(request) context.update({ 'title': "Map Likelihood Fields", @@ -1787,30 +1733,25 @@ 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" context['opts'] = self.model._meta return render(request, "admin/import_csv.html", context) -@admin.register(CampaignSettings) -class CampaignSettingsAdmin(admin.ModelAdmin): - list_display = ('tenant', 'donation_goal', 'twilio_from_number', 'timezone') - list_filter = ('tenant',) - fields = ('tenant', 'donation_goal', 'twilio_account_sid', 'twilio_auth_token', 'twilio_from_number', 'timezone') @admin.register(VotingRecord) class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin): list_display = ('voter', 'election_date', 'election_description', 'primary_party') - list_filter = ('voter__tenant', 'election_date', 'primary_party') + list_filter = ('voter__tenant', 'primary_party') search_fields = ('voter__first_name', 'voter__last_name', 'voter__voter_id', 'election_description') - autocomplete_fields = ["voter"] - change_list_template = "admin/votingrecord_change_list.html" + change_list_template = 'admin/votingrecord_change_list.html' def changelist_view(self, request, extra_context=None): extra_context = extra_context or {} from core.models import Tenant - extra_context["tenants"] = Tenant.objects.all() + extra_context['tenants'] = Tenant.objects.all() return super().changelist_view(request, extra_context=extra_context) def get_urls(self): @@ -1821,204 +1762,181 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin): ] return my_urls + urls - def import_voting_records(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 VOTING_RECORD_MAPPABLE_FIELDS if request.POST.get(f"map_{k}")} - + mapping = {} + 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-sig') as f: - total_count = sum(1 for line in f) - 1 - f.seek(0) + with open(file_path, 'r', encoding='UTF-8') as f: reader = csv.DictReader(f) - preview_rows = [] - voter_ids_for_preview = set() - - v_id_col = mapping.get('voter_id') - ed_col = mapping.get('election_date') - desc_col = mapping.get('election_description') - - if not v_id_col or not ed_col or not desc_col: - raise ValueError("Missing mapping for Voter ID, Election Date, or Description") - - for i, row in enumerate(reader): - if i < 10: - preview_rows.append(row) - v_id = row.get(v_id_col) - if v_id: voter_ids_for_preview.add(str(v_id).strip()) - else: - break - - existing_records = set(VotingRecord.objects.filter( - voter__tenant=tenant, - voter__voter_id__in=voter_ids_for_preview - ).values_list("voter__voter_id", "election_date", "election_description")) - + total_count = 0 + create_count = 0 + update_count = 0 preview_data = [] - for row in preview_rows: - v_id = str(row.get(v_id_col, '')).strip() - e_date_raw = row.get(ed_col) - e_desc = str(row.get(desc_col, '')).strip() + for row in reader: + total_count += 1 + voter_id = row.get(mapping.get('voter_id')) + election_date = row.get(mapping.get('election_date')) - 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 + exists = False + if voter_id and election_date: + try: + # Assuming voter_id and election_date uniquely identify a voting record + # This might need refinement based on actual data uniqueness requirements + if '/' in election_date: + dt = datetime.strptime(election_date, '%m/%d/%Y').date() + elif '-' in election_date: + dt = datetime.strptime(election_date, '%Y-%m-%d').date() + else: + dt = None + + 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 - action = "update" if (v_id, e_date, e_desc) in existing_records else "create" - preview_data.append({ - "action": action, - "identifier": f"Voter: {v_id}, Election: {e_desc}", - "details": f"Date: {e_date or e_date_raw}" - }) - + if exists: + update_count += 1 + action = 'update' + else: + create_count += 1 + action = 'create' + + if len(preview_data) < 10: + preview_data.append({ + 'action': action, + 'identifier': f"Voter ID: {voter_id} (Election: {election_date})", + 'details': f"Party: {row.get(mapping.get('primary_party', '')) 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, + 'title': "Import Preview", + 'total_count': total_count, + 'create_count': create_count, + 'update_count': update_count, + '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("..") + return redirect("..\\n") 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 VOTING_RECORD_MAPPABLE_FIELDS if request.POST.get(f"map_{k}")} + + mapping = {} + for field_name, _ in VOTING_RECORD_MAPPABLE_FIELDS: + mapping[field_name] = request.POST.get(f'map_{field_name}') try: count = 0 - created_count = 0 - updated_count = 0 - skipped_no_change = 0 errors = 0 - batch_size = 2000 + failed_rows = [] + with open(file_path, 'r', encoding='UTF-8') as f: + reader = csv.DictReader(f) + for row in reader: + try: + voter_id = row.get(mapping.get('voter_id')) + election_date_str = row.get(mapping.get('election_date')) + election_description = row.get(mapping.get('election_description')) + primary_party = row.get(mapping.get('primary_party')) - with open(file_path, "r", encoding="utf-8-sig") as 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") - party_col = mapping.get("primary_party") - - if not v_id_col or not ed_col or not desc_col: - raise ValueError("Missing mapping for Voter ID, Election Date, or Description") - - 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) + if not voter_id: + row["Import Error"] = "Missing voter ID" + failed_rows.append(row) + errors += 1 + continue - total_processed = 0 - for chunk in self.chunk_reader(raw_reader, batch_size): - with transaction.atomic(): - 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")} + if not election_date_str or not election_description: + row["Import Error"] = "Missing election date or description" + failed_rows.append(row) + errors += 1 + continue - 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 - ).only("id", "election_date", "election_description", "voter__voter_id").select_related("voter") - } - - to_create = [] - to_update = [] - processed_in_batch = set() + 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 - for row in chunk: - total_processed += 1 - try: - 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 v_id or not raw_ed or not desc: continue - - 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: - 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)) - if not vr: - to_create.append(VotingRecord(voter=voter, election_date=e_date, election_description=desc, primary_party=party)) - created_count += 1 - 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: + try: + if '/' in election_date_str: + parsed_election_date = datetime.strptime(election_date_str, '%m/%d/%Y').date() + elif '-' in election_date_str: + parsed_election_date = datetime.strptime(election_date_str, '%Y-%m-%d').date() + else: + row["Import Error"] = "Invalid date format" + failed_rows.append(row) errors += 1 + continue + except ValueError: + row["Import Error"] = "Invalid date format" + failed_rows.append(row) + errors += 1 + continue - 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.") - + VotingRecord.objects.update_or_create( + voter=voter, + election_date=parsed_election_date, + defaults={ + 'election_description': election_description, + 'primary_party': primary_party or '' + } + ) + count += 1 + except Exception as e: + logger.error(f"Error importing: {e}") + 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} voting records created/updated. ({created_count} new, {updated_count} updated, {skipped_no_change} skipped, {errors} errors)") - return redirect("..") + self.message_user(request, f"Successfully imported {count} voting records.") + request.session[f"{self.model._meta.model_name}_import_errors"] = failed_rows[:1000] + 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) + return redirect("..\\n") except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) - return redirect("..") + return redirect("..\\n") 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("..") + return redirect("..\\n") + 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: + + with open(file_path, 'r', encoding='UTF-8') as f: reader = csv.reader(f) headers = next(reader) + context = self.admin_site.each_context(request) context.update({ 'title': "Map Voting Record Fields", @@ -2032,8 +1950,9 @@ 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" context['opts'] = self.model._meta - return render(request, "admin/import_csv.html", context) + return render(request, "admin/import_csv.html", context) \ No newline at end of file diff --git a/core/forms.py b/core/forms.py index aa2c26d..e3764db 100644 --- a/core/forms.py +++ b/core/forms.py @@ -1,7 +1,6 @@ from django import forms from django.contrib.auth.models import User from .models import Voter, Interaction, Donation, VoterLikelihood, InteractionType, DonationMethod, ElectionType, Event, EventParticipation, EventType, Tenant, ParticipationStatus, Volunteer, VolunteerEvent, VolunteerRole, ScheduledCall -from .permissions import get_user_role class Select2MultipleWidget(forms.SelectMultiple): """ @@ -277,14 +276,56 @@ class EventImportForm(forms.Form): self.fields['file'].widget.attrs.update({'class': 'form-control'}) class EventParticipationImportForm(forms.Form): - tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), label="Campaign") - file = forms.FileField(label="Select CSV file") + file = forms.FileField(label="Select CSV/Excel file") - def __init__(self, *args, **kwargs): + def __init__(self, *args, event=None, **kwargs): super().__init__(*args, **kwargs) - self.fields['tenant'].widget.attrs.update({'class': 'form-control form-select'}) + # No tenant field needed as event_id is passed directly self.fields['file'].widget.attrs.update({'class': 'form-control'}) +class ParticipantMappingForm(forms.Form): + def __init__(self, *args, headers, tenant, **kwargs): + super().__init__(*args, **kwargs) + self.fields['email_column'] = forms.ChoiceField( + choices=[(header, header) for header in headers], + label="Column for Email Address", + required=True, + widget=forms.Select(attrs={'class': 'form-select'}) + ) + + name_choices = [('', '-- Select Name Column (Optional) --')] + [(header, header) for header in headers] + self.fields['name_column'] = forms.ChoiceField( + choices=name_choices, + label="Column for Participant Name", + required=False, + widget=forms.Select(attrs={'class': 'form-select'}) + ) + + phone_choices = [('', '-- Select Phone Column (Optional) --')] + [(header, header) for header in headers] + self.fields['phone_column'] = forms.ChoiceField( + choices=phone_choices, + label="Column for Phone Number", + required=False, + widget=forms.Select(attrs={'class': 'form-select'}) + ) + + participation_status_choices = [('', '-- Select Status Column (Optional) --')] + [(header, header) for header in headers] + self.fields['participation_status_column'] = forms.ChoiceField( + choices=participation_status_choices, + label="Column for Participation Status", + required=False, + widget=forms.Select(attrs={'class': 'form-select'}) + ) + + # Optional: Add a default participation status if no column is mapped + self.fields['default_participation_status'] = forms.ModelChoiceField( + queryset=ParticipationStatus.objects.filter(tenant=tenant, is_active=True), + label="Default Participation Status (if no column mapped or column is empty)", + required=False, + empty_label="-- Select a Default Status --", + widget=forms.Select(attrs={'class': 'form-select'}) + ) + class DonationImportForm(forms.Form): tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), label="Campaign") file = forms.FileField(label="Select CSV file") @@ -456,4 +497,5 @@ class VolunteerProfileForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for field in self.fields.values(): - field.widget.attrs.update({'class': 'form-control'}) \ No newline at end of file + field.widget.attrs.update({'class': 'form-control'} +) \ No newline at end of file diff --git a/core/templates/admin/import_preview.html b/core/templates/admin/import_preview.html index 1bf9ca8..ba17685 100644 --- a/core/templates/admin/import_preview.html +++ b/core/templates/admin/import_preview.html @@ -31,7 +31,7 @@ {% translate "Action" %} - {% translate "Identifyer" %} + {% translate "CSV Name / Matched Voter" %} {% translate "Details" %} @@ -45,7 +45,17 @@ {% translate "UPDATE" %} {% endif %} - {{ row.identifier }} + + {% if row.csv_full_name %} + CSV: {{ row.csv_full_name }} + {% if "Voter: N/A" not in row.identifier %}
{% endif %} + {% endif %} + {% if "Voter: N/A" not in row.identifier %} + Matched: {{ row.identifier|cut:"Voter: " }} + {% else %} + {% if not row.csv_full_name %}N/A{% endif %} + {% endif %} + {{ row.details }} {% endfor %} @@ -74,4 +84,4 @@ -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/event_detail.html b/core/templates/core/event_detail.html index 9003941..abcb681 100644 --- a/core/templates/core/event_detail.html +++ b/core/templates/core/event_detail.html @@ -20,6 +20,9 @@ + @@ -247,6 +250,31 @@ + + +