diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index d4f4494..950bfbe 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index 33120a9..8bace11 100644 Binary files a/core/__pycache__/forms.cpython-311.pyc and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 9a15037..54e8551 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index c8cfc40..156fa51 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index e1d3803..51dd659 100644 --- a/core/admin.py +++ b/core/admin.py @@ -19,7 +19,7 @@ from .models import ( Interest, Volunteer, VolunteerEvent, ParticipationStatus, VolunteerRole ) from .forms import ( - VoterImportForm, EventImportForm, EventParticipationImportForm, + VoterImportForm, EventImportForm, EventParticipationImportForm, DonationImportForm, InteractionImportForm, VoterLikelihoodImportForm, VolunteerImportForm, VotingRecordImportForm ) @@ -126,7 +126,7 @@ class BaseImportAdminMixin: failed_rows = request.session.get(session_key, []) if not failed_rows: self.message_user(request, "No error log found in session.", level=messages.WARNING) - return redirect("..\\n") + return redirect("../") response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = f"attachment; filename={self.model._meta.model_name}_import_errors.csv" @@ -252,7 +252,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): inlines = [VotingRecordInline, DonationInline, InteractionInline, VoterLikelihoodInline] readonly_fields = ('address',) change_list_template = "admin/voter_change_list.html" - + def changelist_view(self, request, extra_context=None): extra_context = extra_context or {} from core.models import Tenant @@ -274,7 +274,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): file_path = request.POST.get("file_path") tenant_id = request.POST.get("tenant") tenant = Tenant.objects.get(id=tenant_id) - + mapping = {} for field_name, _ in VOTER_MAPPABLE_FIELDS: mapping[field_name] = request.POST.get(f"map_{field_name}") @@ -294,18 +294,18 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): preview_rows.append(row) v_id = row.get(mapping.get("voter_id")) if v_id: - voter_ids_for_preview.append(v_id) + voter_ids_for_preview.append(v_id.strip()) else: break - + existing_preview_ids = set(Voter.objects.filter(tenant=tenant, voter_id__in=voter_ids_for_preview).values_list("voter_id", flat=True)) - + create_count = 0 update_count = 0 - + for row in preview_rows: voter_id_val = row.get(mapping.get("voter_id")) - if voter_id_val in existing_preview_ids: + if voter_id_val and voter_id_val.strip() in existing_preview_ids: update_count += 1 else: create_count += 1 @@ -326,12 +326,12 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): return render(request, "admin/import_preview.html", context) except Exception as e: self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") elif "_import" in request.POST: file_path = request.POST.get("file_path") tenant_id = request.POST.get("tenant") tenant = Tenant.objects.get(id=tenant_id) - + mapping = {} for field_name, _ in VOTER_MAPPABLE_FIELDS: mapping[field_name] = request.POST.get(f"map_{field_name}") @@ -355,9 +355,14 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): for i, row in enumerate(reader): total_processed += 1 try: - voter_id = row.get(mapping.get("voter_id")) + raw_voter_id = row.get(mapping.get("voter_id")) + voter_id = raw_voter_id.strip() if raw_voter_id else None + if not voter_id: - row["Import Error"] = "Voter ID is required" + # Enhanced error message to guide the user + mapped_column_name = mapping.get("voter_id", "N/A") + error_detail = f"Raw value: '{raw_voter_id}'. " if raw_voter_id is not None else "Value was None." + row["Import Error"] = f"Voter ID is required. Please check if the '{mapped_column_name}' column is correctly mapped and contains values for all rows. {error_detail}" failed_rows.append(row) skipped_no_id += 1 errors += 1 @@ -446,7 +451,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): created_count += 1 else: updated_count += 1 - + # Special handling for interests - assuming a comma-separated list in CSV if 'interests' in mapping and row.get(mapping['interests']): interest_names = [name.strip() for name in row[mapping['interests']].split(',') if name.strip()] @@ -478,26 +483,26 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): if errors > 0: error_url = reverse("admin:voter-download-errors") self.message_user(request, mark_safe(f"Failed to import {errors} rows. Download failed records"), level=messages.WARNING) - return redirect("..\\n") + return redirect("../") except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") else: form = VoterImportForm(request.POST, request.FILES) if form.is_valid(): csv_file = request.FILES['file'] tenant = form.cleaned_data['tenant'] - + if not csv_file.name.endswith('.csv'): self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: for chunk in csv_file.chunks(): tmp.write(chunk) file_path = tmp.name - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.reader(f) headers = next(reader) @@ -514,7 +519,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): return render(request, "admin/import_mapping.html", context) else: form = VoterImportForm() - + context = self.admin_site.each_context(request) context['form'] = form context['title'] = "Import Voters" @@ -554,7 +559,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): for field_name, _ in EVENT_MAPPABLE_FIELDS: mapping[field_name] = request.POST.get(f'map_{field_name}') try: - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.DictReader(f) total_count = 0 create_count = 0 @@ -564,7 +569,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): total_count += 1 event_name = row.get(mapping.get('name')) event_date = row.get(mapping.get('date')) - + exists = False if event_name and event_date: try: @@ -579,18 +584,18 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): if dt: exists = Event.objects.filter(tenant=tenant, name=event_name, date=dt).exists() - + except ValueError: # Handle cases where date parsing fails pass - + if exists: update_count += 1 action = 'update' else: create_count += 1 action = 'create' - + if len(preview_data) < 10: preview_data.append({ 'action': action, @@ -613,13 +618,13 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): return render(request, "admin/import_preview.html", context) except Exception as e: self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") elif "_import" in request.POST: file_path = request.POST.get('file_path') tenant_id = request.POST.get('tenant') tenant = Tenant.objects.get(id=tenant_id) - + mapping = {} for field_name, _ in EVENT_MAPPABLE_FIELDS: mapping[field_name] = request.POST.get(f'map_{field_name}') @@ -628,7 +633,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): count = 0 errors = 0 failed_rows = [] - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.DictReader(f) for row in reader: try: @@ -658,7 +663,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): failed_rows.append(row) errors += 1 continue - + event_type_obj, _ = EventType.objects.get_or_create(tenant=tenant, name=event_type_name) defaults = { @@ -699,7 +704,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): row["Import Error"] = str(e) failed_rows.append(row) errors += 1 - + if os.path.exists(file_path): os.remove(file_path) self.message_user(request, f"Successfully imported {count} events.") @@ -708,26 +713,26 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): if errors > 0: error_url = reverse("admin:event-download-errors") self.message_user(request, mark_safe(f"Failed to import {errors} rows. Download failed records"), level=messages.WARNING) - return redirect("..\\n") + return redirect("../") except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") else: form = EventImportForm(request.POST, request.FILES) if form.is_valid(): csv_file = request.FILES['file'] tenant = form.cleaned_data['tenant'] - + if not csv_file.name.endswith('.csv'): self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: for chunk in csv_file.chunks(): tmp.write(chunk) file_path = tmp.name - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.reader(f) headers = next(reader) @@ -744,7 +749,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): return render(request, "admin/import_mapping.html", context) else: form = EventImportForm() - + context = self.admin_site.each_context(request) context['form'] = form context['title'] = "Import Events" @@ -784,7 +789,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin): mapping[field_name] = request.POST.get(f'map_{field_name}') try: - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.DictReader(f) total_count = 0 create_count = 0 @@ -797,14 +802,14 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin): exists = False if email: exists = Volunteer.objects.filter(tenant=tenant, email=email).exists() - + if exists: update_count += 1 action = 'update' else: create_count += 1 action = 'create' - + if len(preview_data) < 10: preview_data.append({ 'action': action, @@ -827,13 +832,13 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin): return render(request, "admin/import_preview.html", context) except Exception as e: self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") elif "_import" in request.POST: file_path = request.POST.get('file_path') tenant_id = request.POST.get('tenant') tenant = Tenant.objects.get(id=tenant_id) - + mapping = {} for field_name, _ in VOLUNTEER_MAPPABLE_FIELDS: mapping[field_name] = request.POST.get(f'map_{field_name}') @@ -842,7 +847,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin): count = 0 errors = 0 failed_rows = [] - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.DictReader(f) for row in reader: try: @@ -852,7 +857,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin): failed_rows.append(row) errors += 1 continue - + defaults = { 'first_name': row.get(mapping.get('first_name')) or '', 'last_name': row.get(mapping.get('last_name')) or '', @@ -871,7 +876,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin): row["Import Error"] = str(e) failed_rows.append(row) errors += 1 - + if os.path.exists(file_path): os.remove(file_path) self.message_user(request, f"Successfully imported {count} volunteers.") @@ -880,26 +885,26 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin): if errors > 0: error_url = reverse("admin:volunteer-download-errors") self.message_user(request, mark_safe(f"Failed to import {errors} rows. Download failed records"), level=messages.WARNING) - return redirect("..\\n") + return redirect("../") except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") else: form = VolunteerImportForm(request.POST, request.FILES) if form.is_valid(): csv_file = request.FILES['file'] tenant = form.cleaned_data['tenant'] - + if not csv_file.name.endswith('.csv'): self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: for chunk in csv_file.chunks(): tmp.write(chunk) file_path = tmp.name - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.reader(f) headers = next(reader) @@ -916,7 +921,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin): return render(request, "admin/import_mapping.html", context) else: form = VolunteerImportForm() - + context = self.admin_site.each_context(request) context['form'] = form context['title'] = "Import Volunteers" @@ -935,7 +940,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin): extra_context = extra_context or {} from core.models import Tenant extra_context['tenants'] = Tenant.objects.all() - return super().changelist_view(request, extra_context=extra_context) + return super().changelist_list(request, extra_context=extra_context) def get_urls(self): urls = super().get_urls() @@ -955,7 +960,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin): for field_name, _ in EVENT_PARTICIPATION_MAPPABLE_FIELDS: mapping[field_name] = request.POST.get(f'map_{field_name}') try: - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.DictReader(f) total_count = 0 create_count = 0 @@ -964,8 +969,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin): for row in reader: total_count += 1 voter_id = row.get(mapping.get('voter_id')) - event_name = row.get(mapping.get('event_name')) - + # Extract first_name and last_name from CSV based on mapping csv_first_name = row.get(mapping.get('first_name'), '') csv_last_name = row.get(mapping.get('last_name'), '') @@ -981,14 +985,14 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin): exists = EventParticipation.objects.filter(voter=voter, event__name=event_name).exists() except Voter.DoesNotExist: pass - + if exists: update_count += 1 action = 'update' else: create_count += 1 action = 'create' - + if len(preview_data) < 10: preview_data.append({ 'action': action, @@ -1012,19 +1016,19 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin): return render(request, "admin/import_preview.html", context) except Exception as e: self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") elif "_import" in request.POST: file_path = request.POST.get('file_path') tenant_id = request.POST.get('tenant') tenant = Tenant.objects.get(id=tenant_id) - + mapping = {} for field_name, _ in EVENT_PARTICIPATION_MAPPABLE_FIELDS: mapping[field_name] = request.POST.get(f'map_{field_name}') try: - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.DictReader(f) count = 0 errors = 0 @@ -1033,13 +1037,16 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin): try: voter_id = row.get(mapping.get('voter_id')) if mapping.get('voter_id') else None participation_status_val = row.get(mapping.get('participation_status')) if mapping.get('participation_status') else None - + + if voter_id: # Only strip if voter_id is not None + voter_id = voter_id.strip() + if not voter_id: row["Import Error"] = "Missing voter ID" failed_rows.append(row) errors += 1 continue - + try: voter = Voter.objects.get(tenant=tenant, voter_id=voter_id) except Voter.DoesNotExist: @@ -1085,7 +1092,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin): row["Import Error"] = str(e) failed_rows.append(row) errors += 1 - + if os.path.exists(file_path): os.remove(file_path) self.message_user(request, f"Successfully imported {count} participations.") @@ -1096,26 +1103,26 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin): if errors > 0: error_url = reverse("admin:eventparticipation-download-errors") self.message_user(request, mark_safe(f"Failed to import {errors} rows. Download failed records"), level=messages.WARNING) - return redirect("..\\n") + return redirect("../") except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") else: form = EventParticipationImportForm(request.POST, request.FILES) if form.is_valid(): csv_file = request.FILES['file'] tenant = form.cleaned_data['tenant'] - + if not csv_file.name.endswith('.csv'): self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: for chunk in csv_file.chunks(): tmp.write(chunk) file_path = tmp.name - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.reader(f) headers = next(reader) @@ -1132,7 +1139,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin): return render(request, "admin/import_mapping.html", context) else: form = EventParticipationImportForm() - + context = self.admin_site.each_context(request) context['form'] = form context['title'] = "Import Participations" @@ -1170,7 +1177,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): for field_name, _ in DONATION_MAPPABLE_FIELDS: mapping[field_name] = request.POST.get(f'map_{field_name}') try: - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.DictReader(f) total_count = 0 create_count = 0 @@ -1179,18 +1186,18 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): for row in reader: total_count += 1 voter_id = row.get(mapping.get('voter_id')) - + exists = False if voter_id: exists = Voter.objects.filter(tenant=tenant, voter_id=voter_id).exists() - + if exists: update_count += 1 action = 'update' else: create_count += 1 action = 'create' - + if len(preview_data) < 10: preview_data.append({ 'action': action, @@ -1213,13 +1220,13 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): return render(request, "admin/import_preview.html", context) except Exception as e: self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") elif "_import" in request.POST: file_path = request.POST.get('file_path') tenant_id = request.POST.get('tenant') tenant = Tenant.objects.get(id=tenant_id) - + mapping = {} for field_name, _ in DONATION_MAPPABLE_FIELDS: mapping[field_name] = request.POST.get(f'map_{field_name}') @@ -1228,7 +1235,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): count = 0 errors = 0 failed_rows = [] - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.DictReader(f) for row in reader: try: @@ -1237,6 +1244,9 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): amount_str = row.get(mapping.get('amount')) method_name = row.get(mapping.get('method')) + if voter_id: # Only strip if voter_id is not None + voter_id = voter_id.strip() + if not voter_id: row["Import Error"] = "Missing voter ID" failed_rows.append(row) @@ -1248,7 +1258,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): failed_rows.append(row) errors += 1 continue - + try: voter = Voter.objects.get(tenant=tenant, voter_id=voter_id) except Voter.DoesNotExist: @@ -1256,7 +1266,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): failed_rows.append(row) errors += 1 continue - + try: if '/' in date_str: parsed_date = datetime.strptime(date_str, '%m/%d/%Y').date() @@ -1280,7 +1290,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): failed_rows.append(row) errors += 1 continue - + donation_method, _ = DonationMethod.objects.get_or_create(tenant=tenant, name=method_name) Donation.objects.create( @@ -1295,7 +1305,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): row["Import Error"] = str(e) failed_rows.append(row) errors += 1 - + if os.path.exists(file_path): os.remove(file_path) self.message_user(request, f"Successfully imported {count} donations.") @@ -1304,26 +1314,26 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): if errors > 0: error_url = reverse("admin:donation-download-errors") self.message_user(request, mark_safe(f"Failed to import {errors} rows. Download failed records"), level=messages.WARNING) - return redirect("..\\n") + return redirect("../") except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") else: form = DonationImportForm(request.POST, request.FILES) if form.is_valid(): csv_file = request.FILES['file'] tenant = form.cleaned_data['tenant'] - + if not csv_file.name.endswith('.csv'): self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: for chunk in csv_file.chunks(): tmp.write(chunk) file_path = tmp.name - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.reader(f) headers = next(reader) @@ -1340,7 +1350,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): return render(request, "admin/import_mapping.html", context) else: form = DonationImportForm() - + context = self.admin_site.each_context(request) context['form'] = form context['title'] = "Import Donations" @@ -1379,7 +1389,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): for field_name, _ in INTERACTION_MAPPABLE_FIELDS: mapping[field_name] = request.POST.get(f'map_{field_name}') try: - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.DictReader(f) total_count = 0 create_count = 0 @@ -1389,18 +1399,18 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): total_count += 1 voter_id = row.get(mapping.get('voter_id')) volunteer_email = row.get(mapping.get('volunteer_email')) - + exists = False if voter_id: exists = Voter.objects.filter(tenant=tenant, voter_id=voter_id).exists() - + if exists: update_count += 1 action = 'update' else: create_count += 1 action = 'create' - + if len(preview_data) < 10: preview_data.append({ 'action': action, @@ -1423,13 +1433,13 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): return render(request, "admin/import_preview.html", context) except Exception as e: self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") elif "_import" in request.POST: file_path = request.POST.get('file_path') tenant_id = request.POST.get('tenant') tenant = Tenant.objects.get(id=tenant_id) - + mapping = {} for field_name, _ in INTERACTION_MAPPABLE_FIELDS: mapping[field_name] = request.POST.get(f'map_{field_name}') @@ -1438,7 +1448,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): count = 0 errors = 0 failed_rows = [] - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.DictReader(f) for row in reader: try: @@ -1447,6 +1457,9 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): date_str = row.get(mapping.get('date')) type_name = row.get(mapping.get('type')) + if voter_id: # Only strip if voter_id is not None + voter_id = voter_id.strip() + if not voter_id: row["Import Error"] = "Missing voter ID" failed_rows.append(row) @@ -1458,7 +1471,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): failed_rows.append(row) errors += 1 continue - + try: voter = Voter.objects.get(tenant=tenant, voter_id=voter_id) except Voter.DoesNotExist: @@ -1466,7 +1479,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): failed_rows.append(row) errors += 1 continue - + volunteer = None if volunteer_email: try: @@ -1506,7 +1519,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): row["Import Error"] = str(e) failed_rows.append(row) errors += 1 - + if os.path.exists(file_path): os.remove(file_path) self.message_user(request, f"Successfully imported {count} interactions.") @@ -1515,26 +1528,26 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): if errors > 0: error_url = reverse("admin:interaction-download-errors") self.message_user(request, mark_safe(f"Failed to import {errors} rows. Download failed records"), level=messages.WARNING) - return redirect("..\\n") + return redirect("../") except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") else: form = InteractionImportForm(request.POST, request.FILES) if form.is_valid(): csv_file = request.FILES['file'] tenant = form.cleaned_data['tenant'] - + if not csv_file.name.endswith('.csv'): self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: for chunk in csv_file.chunks(): tmp.write(chunk) file_path = tmp.name - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.reader(f) headers = next(reader) @@ -1551,7 +1564,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): return render(request, "admin/import_mapping.html", context) else: form = InteractionImportForm() - + context = self.admin_site.each_context(request) context['form'] = form context['title'] = "Import Interactions" @@ -1589,7 +1602,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin): for field_name, _ in VOTER_LIKELIHOOD_MAPPABLE_FIELDS: mapping[field_name] = request.POST.get(f'map_{field_name}') try: - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.DictReader(f) total_count = 0 create_count = 0 @@ -1598,18 +1611,18 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin): for row in reader: total_count += 1 voter_id = row.get(mapping.get('voter_id')) - + exists = False if voter_id: exists = Voter.objects.filter(tenant=tenant, voter_id=voter_id).exists() - + if exists: update_count += 1 action = 'update' else: create_count += 1 action = 'create' - + if len(preview_data) < 10: preview_data.append({ 'action': action, @@ -1632,13 +1645,13 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin): return render(request, "admin/import_preview.html", context) except Exception as e: self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") elif "_import" in request.POST: file_path = request.POST.get('file_path') tenant_id = request.POST.get('tenant') tenant = Tenant.objects.get(id=tenant_id) - + mapping = {} for field_name, _ in VOTER_LIKELIHOOD_MAPPABLE_FIELDS: mapping[field_name] = request.POST.get(f'map_{field_name}') @@ -1647,7 +1660,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin): count = 0 errors = 0 failed_rows = [] - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.DictReader(f) for row in reader: try: @@ -1655,12 +1668,15 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin): election_type_name = row.get(mapping.get('election_type')) likelihood_val = row.get(mapping.get('likelihood')) + if voter_id: # Only strip if voter_id is not None + voter_id = voter_id.strip() + if not voter_id: row["Import Error"] = "Missing voter ID" failed_rows.append(row) errors += 1 continue - + if not election_type_name or not likelihood_val: row["Import Error"] = "Missing election type or likelihood" failed_rows.append(row) @@ -1674,7 +1690,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin): failed_rows.append(row) errors += 1 continue - + election_type, _ = ElectionType.objects.get_or_create(tenant=tenant, name=election_type_name) VoterLikelihood.objects.update_or_create( @@ -1688,7 +1704,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin): row["Import Error"] = str(e) failed_rows.append(row) errors += 1 - + if os.path.exists(file_path): os.remove(file_path) self.message_user(request, f"Import complete: {count} likelihoods created/updated.") @@ -1697,26 +1713,26 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin): if errors > 0: error_url = reverse("admin:voterlikelihood-download-errors") self.message_user(request, mark_safe(f"Failed to import {errors} rows. Download failed records"), level=messages.WARNING) - return redirect("..\\n") + return redirect("../") except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") else: form = VoterLikelihoodImportForm(request.POST, request.FILES) if form.is_valid(): csv_file = request.FILES['file'] tenant = form.cleaned_data['tenant'] - + if not csv_file.name.endswith('.csv'): self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: for chunk in csv_file.chunks(): tmp.write(chunk) file_path = tmp.name - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.reader(f) headers = next(reader) @@ -1733,7 +1749,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin): return render(request, "admin/import_mapping.html", context) else: form = VoterLikelihoodImportForm() - + context = self.admin_site.each_context(request) context['form'] = form context['title'] = "Import Likelihoods" @@ -1772,7 +1788,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin): for field_name, _ in VOTING_RECORD_MAPPABLE_FIELDS: mapping[field_name] = request.POST.get(f'map_{field_name}') try: - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.DictReader(f) total_count = 0 create_count = 0 @@ -1782,7 +1798,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin): total_count += 1 voter_id = row.get(mapping.get('voter_id')) election_date = row.get(mapping.get('election_date')) - + exists = False if voter_id and election_date: try: @@ -1797,18 +1813,18 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin): if dt: exists = VotingRecord.objects.filter(voter__tenant=tenant, voter__voter_id=voter_id, election_date=dt).exists() - + except ValueError: # Handle cases where date parsing fails pass - + if exists: update_count += 1 action = 'update' else: create_count += 1 action = 'create' - + if len(preview_data) < 10: preview_data.append({ 'action': action, @@ -1831,13 +1847,13 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin): return render(request, "admin/import_preview.html", context) except Exception as e: self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") elif "_import" in request.POST: file_path = request.POST.get('file_path') tenant_id = request.POST.get('tenant') tenant = Tenant.objects.get(id=tenant_id) - + mapping = {} for field_name, _ in VOTING_RECORD_MAPPABLE_FIELDS: mapping[field_name] = request.POST.get(f'map_{field_name}') @@ -1846,7 +1862,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin): count = 0 errors = 0 failed_rows = [] - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.DictReader(f) for row in reader: try: @@ -1855,6 +1871,9 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin): election_description = row.get(mapping.get('election_description')) primary_party = row.get(mapping.get('primary_party')) + if voter_id: # Only strip if voter_id is not None + voter_id = voter_id.strip() + if not voter_id: row["Import Error"] = "Missing voter ID" failed_rows.append(row) @@ -1866,7 +1885,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin): failed_rows.append(row) errors += 1 continue - + try: voter = Voter.objects.get(tenant=tenant, voter_id=voter_id) except Voter.DoesNotExist: @@ -1905,7 +1924,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin): row["Import Error"] = str(e) failed_rows.append(row) errors += 1 - + if os.path.exists(file_path): os.remove(file_path) self.message_user(request, f"Successfully imported {count} voting records.") @@ -1914,26 +1933,26 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin): if errors > 0: error_url = reverse("admin:votingrecord-download-errors") self.message_user(request, mark_safe(f"Failed to import {errors} rows. Download failed records"), level=messages.WARNING) - return redirect("..\\n") + return redirect("../") except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") else: form = VotingRecordImportForm(request.POST, request.FILES) if form.is_valid(): csv_file = request.FILES['file'] tenant = form.cleaned_data['tenant'] - + if not csv_file.name.endswith('.csv'): self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) - return redirect("..\\n") + return redirect("../") with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: for chunk in csv_file.chunks(): tmp.write(chunk) file_path = tmp.name - with open(file_path, 'r', encoding='UTF-8') as f: + with open(file_path, 'r', encoding='utf-8-sig') as f: reader = csv.reader(f) headers = next(reader) @@ -1950,7 +1969,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin): return render(request, "admin/import_mapping.html", context) else: form = VotingRecordImportForm() - + context = self.admin_site.each_context(request) context['form'] = form context['title'] = "Import Voting Records" diff --git a/core/forms.py b/core/forms.py index e3764db..670ed7f 100644 --- a/core/forms.py +++ b/core/forms.py @@ -118,6 +118,7 @@ class AdvancedVoterSearchForm(forms.Form): neighborhood = forms.CharField(required=False) district = forms.CharField(required=False) precinct = forms.CharField(required=False) + email = forms.EmailField(required=False) # Added email field phone_type = forms.ChoiceField( choices=[('', 'Any')] + Voter.PHONE_TYPE_CHOICES, required=False @@ -125,7 +126,7 @@ class AdvancedVoterSearchForm(forms.Form): is_targeted = forms.BooleanField(required=False, label="Targeted Only") door_visit = forms.BooleanField(required=False, label="Visited Only") candidate_support = forms.ChoiceField( - choices=[('', 'Any')] + Voter.SUPPORT_CHOICES, + choices=[('', 'Any')] + Voter.CANDIDATE_SUPPORT_CHOICES, required=False ) yard_sign = forms.ChoiceField( @@ -443,7 +444,7 @@ class DoorVisitLogForm(forms.Form): label="Wants a Yard Sign" ) candidate_support = forms.ChoiceField( - choices=Voter.SUPPORT_CHOICES, + choices=Voter.CANDIDATE_SUPPORT_CHOICES, initial="unknown", widget=forms.Select(attrs={"class": "form-select"}), label="Candidate Support" @@ -484,16 +485,6 @@ class UserUpdateForm(forms.ModelForm): model = User fields = ['first_name', 'last_name', 'email'] - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - for field in self.fields.values(): - field.widget.attrs.update({'class': 'form-control'}) - -class VolunteerProfileForm(forms.ModelForm): - class Meta: - model = Volunteer - fields = ['phone'] - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for field in self.fields.values(): diff --git a/core/models.py b/core/models.py index 4723cea..032d9ec 100644 --- a/core/models.py +++ b/core/models.py @@ -125,7 +125,7 @@ class Interest(models.Model): return self.name class Voter(models.Model): - SUPPORT_CHOICES = [ + CANDIDATE_SUPPORT_CHOICES = [ ('unknown', 'Unknown'), ('supporting', 'Supporting'), ('not_supporting', 'Not Supporting'), @@ -170,7 +170,7 @@ class Voter(models.Model): precinct = models.CharField(max_length=100, blank=True, db_index=True) registration_date = models.DateField(null=True, blank=True) is_targeted = models.BooleanField(default=False, db_index=True) - candidate_support = models.CharField(max_length=20, choices=SUPPORT_CHOICES, default='unknown', db_index=True) + candidate_support = models.CharField(max_length=20, choices=CANDIDATE_SUPPORT_CHOICES, default='unknown', db_index=True) yard_sign = models.CharField(max_length=20, choices=YARD_SIGN_CHOICES, default='none', db_index=True) window_sticker = models.CharField(max_length=20, choices=WINDOW_STICKER_CHOICES, default='none', verbose_name='Window Sticker Status', db_index=True) notes = models.TextField(blank=True) diff --git a/core/templates/core/voter_advanced_search.html b/core/templates/core/voter_advanced_search.html index 6305f05..769d26f 100644 --- a/core/templates/core/voter_advanced_search.html +++ b/core/templates/core/voter_advanced_search.html @@ -49,6 +49,10 @@ {{ form.precinct }} +