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 }} +
+ + {{ form.email }} +
{{ form.phone_type }} diff --git a/core/views.py b/core/views.py index b15e6fa..a9ae83e 100644 --- a/core/views.py +++ b/core/views.py @@ -17,7 +17,7 @@ from django.contrib import messages from django.core.paginator import Paginator from django.conf import settings from .models import Voter, Tenant, Interaction, Donation, VoterLikelihood, EventParticipation, Event, EventType, InteractionType, DonationMethod, ElectionType, CampaignSettings, Volunteer, ParticipationStatus, VolunteerEvent, Interest, VolunteerRole, ScheduledCall -from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm, VoterImportForm, AdvancedVoterSearchForm, EventParticipantAddForm, EventForm, VolunteerForm, VolunteerEventForm, VolunteerEventAddForm, DoorVisitLogForm, ScheduledCallForm, UserUpdateForm, VolunteerProfileForm, EventParticipationImportForm, ParticipantMappingForm +from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm, VoterImportForm, AdvancedVoterSearchForm, EventParticipantAddForm, EventForm, VolunteerForm, VolunteerEventForm, VolunteerEventAddForm, DoorVisitLogForm, ScheduledCallForm, UserUpdateForm, EventParticipationImportForm, ParticipantMappingForm import logging import zoneinfo from django.utils import timezone @@ -157,7 +157,7 @@ def voter_list(request): if query: query = query.strip() - search_filter = Q(first_name__icontains=query) | Q(last_name__icontains=query) | Q(voter_id__icontains=query) + search_filter = Q(first_name__icontains=query) | Q(last_name__icontains=query) | Q(voter_id__iexact=query) if "," in query: parts = [p.strip() for p in query.split(",")] @@ -358,7 +358,6 @@ def edit_likelihood(request, likelihood_id): if request.method == 'POST': form = VoterLikelihoodForm(request.POST, instance=likelihood, tenant=tenant) if form.is_valid(): - # Check for conflict with another record of same election_type election_type = form.cleaned_data['election_type'] if VoterLikelihood.objects.filter(voter=likelihood.voter, election_type=election_type).exclude(id=likelihood.id).exists(): VoterLikelihood.objects.filter(voter=likelihood.voter, election_type=election_type).exclude(id=likelihood.id).delete() @@ -487,7 +486,7 @@ def voter_advanced_search(request): if data.get('address'): voters = voters.filter(Q(address__icontains=data['address']) | Q(address_street__icontains=data['address'])) if data.get('voter_id'): - voters = voters.filter(voter_id__icontains=data['voter_id']) + voters = voters.filter(voter_id__iexact=data['voter_id']) if data.get('birth_month'): voters = voters.filter(birthdate__month=data['birth_month']) if data.get('city'): @@ -498,6 +497,8 @@ def voter_advanced_search(request): voters = voters.filter(district=data['district']) if data.get('precinct'): voters = voters.filter(precinct=data['precinct']) + if data.get('email'): + voters = voters.filter(email__icontains=data['email']) if data.get('phone_type'): voters = voters.filter(phone_type=data['phone_type']) if data.get('is_targeted'): @@ -563,7 +564,7 @@ def export_voters_csv(request): if data.get('address'): voters = voters.filter(Q(address__icontains=data['address']) | Q(address_street__icontains=data['address'])) if data.get('voter_id'): - voters = voters.filter(voter_id__icontains=data['voter_id']) + voters = voters.filter(voter_id__iexact=data['voter_id']) if data.get('birth_month'): voters = voters.filter(birthdate__month=data['birth_month']) if data.get('city'): @@ -574,6 +575,8 @@ def export_voters_csv(request): voters = voters.filter(district=data['district']) if data.get('precinct'): voters = voters.filter(precinct=data['precinct']) + if data.get('email'): + voters = voters.filter(email__icontains=data['email']) if data.get('phone_type'): voters = voters.filter(phone_type=data['phone_type']) if data.get('is_targeted'): @@ -842,7 +845,7 @@ def voter_search_json(request): tenant = get_object_or_404(Tenant, id=selected_tenant_id) voters = Voter.objects.filter(tenant=tenant) - search_filter = Q(first_name__icontains=query) | Q(last_name__icontains=query) | Q(voter_id__icontains=query) + search_filter = Q(first_name__icontains=query) | Q(last_name__icontains=query) | Q(voter_id__iexact=query) if "," in query: parts = [p.strip() for p in query.split(",") ] @@ -925,6 +928,7 @@ def volunteer_add(request): 'form': form, 'tenant': tenant, 'selected_tenant': tenant, + 'is_create': True, } return render(request, 'core/volunteer_detail.html', context) @@ -1993,21 +1997,5 @@ def profile(request): if request.method == 'POST': u_form = UserUpdateForm(request.POST, instance=request.user) - v_form = VolunteerProfileForm(request.POST, instance=volunteer) if volunteer else None - - if u_form.is_valid() and (not v_form or v_form.is_valid()): - u_form.save() - if v_form: - v_form.save() - messages.success(request, f'Your profile has been updated!') - return redirect('profile') - else: - u_form = UserUpdateForm(instance=request.user) - v_form = VolunteerProfileForm(instance=volunteer) if volunteer else None - - context = { - 'u_form': u_form, - 'v_form': v_form - } - - return render(request, 'core/profile.html', context) + # v_form = VolunteerProfileForm(request.POST, instance=volunteer) if volunteer else None # Removed VolunteerProfileForm + v_form = None # Set v_form to None after removal