Autosave: 20260206-141042
This commit is contained in:
parent
d244ac9d3f
commit
0d11fc7d5d
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
285
core/admin.py
285
core/admin.py
@ -19,7 +19,7 @@ from .models import (
|
|||||||
Interest, Volunteer, VolunteerEvent, ParticipationStatus, VolunteerRole
|
Interest, Volunteer, VolunteerEvent, ParticipationStatus, VolunteerRole
|
||||||
)
|
)
|
||||||
from .forms import (
|
from .forms import (
|
||||||
VoterImportForm, EventImportForm, EventParticipationImportForm,
|
VoterImportForm, EventImportForm, EventParticipationImportForm,
|
||||||
DonationImportForm, InteractionImportForm, VoterLikelihoodImportForm,
|
DonationImportForm, InteractionImportForm, VoterLikelihoodImportForm,
|
||||||
VolunteerImportForm, VotingRecordImportForm
|
VolunteerImportForm, VotingRecordImportForm
|
||||||
)
|
)
|
||||||
@ -126,7 +126,7 @@ class BaseImportAdminMixin:
|
|||||||
failed_rows = request.session.get(session_key, [])
|
failed_rows = request.session.get(session_key, [])
|
||||||
if not failed_rows:
|
if not failed_rows:
|
||||||
self.message_user(request, "No error log found in session.", level=messages.WARNING)
|
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 = HttpResponse(content_type="text/csv")
|
||||||
response["Content-Disposition"] = f"attachment; filename={self.model._meta.model_name}_import_errors.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]
|
inlines = [VotingRecordInline, DonationInline, InteractionInline, VoterLikelihoodInline]
|
||||||
readonly_fields = ('address',)
|
readonly_fields = ('address',)
|
||||||
change_list_template = "admin/voter_change_list.html"
|
change_list_template = "admin/voter_change_list.html"
|
||||||
|
|
||||||
def changelist_view(self, request, extra_context=None):
|
def changelist_view(self, request, extra_context=None):
|
||||||
extra_context = extra_context or {}
|
extra_context = extra_context or {}
|
||||||
from core.models import Tenant
|
from core.models import Tenant
|
||||||
@ -274,7 +274,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
file_path = request.POST.get("file_path")
|
file_path = request.POST.get("file_path")
|
||||||
tenant_id = request.POST.get("tenant")
|
tenant_id = request.POST.get("tenant")
|
||||||
tenant = Tenant.objects.get(id=tenant_id)
|
tenant = Tenant.objects.get(id=tenant_id)
|
||||||
|
|
||||||
mapping = {}
|
mapping = {}
|
||||||
for field_name, _ in VOTER_MAPPABLE_FIELDS:
|
for field_name, _ in VOTER_MAPPABLE_FIELDS:
|
||||||
mapping[field_name] = request.POST.get(f"map_{field_name}")
|
mapping[field_name] = request.POST.get(f"map_{field_name}")
|
||||||
@ -294,18 +294,18 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
preview_rows.append(row)
|
preview_rows.append(row)
|
||||||
v_id = row.get(mapping.get("voter_id"))
|
v_id = row.get(mapping.get("voter_id"))
|
||||||
if v_id:
|
if v_id:
|
||||||
voter_ids_for_preview.append(v_id)
|
voter_ids_for_preview.append(v_id.strip())
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
existing_preview_ids = set(Voter.objects.filter(tenant=tenant, voter_id__in=voter_ids_for_preview).values_list("voter_id", flat=True))
|
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
|
create_count = 0
|
||||||
update_count = 0
|
update_count = 0
|
||||||
|
|
||||||
for row in preview_rows:
|
for row in preview_rows:
|
||||||
voter_id_val = row.get(mapping.get("voter_id"))
|
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
|
update_count += 1
|
||||||
else:
|
else:
|
||||||
create_count += 1
|
create_count += 1
|
||||||
@ -326,12 +326,12 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
return render(request, "admin/import_preview.html", context)
|
return render(request, "admin/import_preview.html", context)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
|
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
elif "_import" in request.POST:
|
elif "_import" in request.POST:
|
||||||
file_path = request.POST.get("file_path")
|
file_path = request.POST.get("file_path")
|
||||||
tenant_id = request.POST.get("tenant")
|
tenant_id = request.POST.get("tenant")
|
||||||
tenant = Tenant.objects.get(id=tenant_id)
|
tenant = Tenant.objects.get(id=tenant_id)
|
||||||
|
|
||||||
mapping = {}
|
mapping = {}
|
||||||
for field_name, _ in VOTER_MAPPABLE_FIELDS:
|
for field_name, _ in VOTER_MAPPABLE_FIELDS:
|
||||||
mapping[field_name] = request.POST.get(f"map_{field_name}")
|
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):
|
for i, row in enumerate(reader):
|
||||||
total_processed += 1
|
total_processed += 1
|
||||||
try:
|
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:
|
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)
|
failed_rows.append(row)
|
||||||
skipped_no_id += 1
|
skipped_no_id += 1
|
||||||
errors += 1
|
errors += 1
|
||||||
@ -446,7 +451,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
created_count += 1
|
created_count += 1
|
||||||
else:
|
else:
|
||||||
updated_count += 1
|
updated_count += 1
|
||||||
|
|
||||||
# Special handling for interests - assuming a comma-separated list in CSV
|
# Special handling for interests - assuming a comma-separated list in CSV
|
||||||
if 'interests' in mapping and row.get(mapping['interests']):
|
if 'interests' in mapping and row.get(mapping['interests']):
|
||||||
interest_names = [name.strip() for name in row[mapping['interests']].split(',') if name.strip()]
|
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:
|
if errors > 0:
|
||||||
error_url = reverse("admin:voter-download-errors")
|
error_url = reverse("admin:voter-download-errors")
|
||||||
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
|
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
|
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
else:
|
else:
|
||||||
form = VoterImportForm(request.POST, request.FILES)
|
form = VoterImportForm(request.POST, request.FILES)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
csv_file = request.FILES['file']
|
csv_file = request.FILES['file']
|
||||||
tenant = form.cleaned_data['tenant']
|
tenant = form.cleaned_data['tenant']
|
||||||
|
|
||||||
if not csv_file.name.endswith('.csv'):
|
if not csv_file.name.endswith('.csv'):
|
||||||
self.message_user(request, "Please upload a CSV file.", level=messages.ERROR)
|
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:
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp:
|
||||||
for chunk in csv_file.chunks():
|
for chunk in csv_file.chunks():
|
||||||
tmp.write(chunk)
|
tmp.write(chunk)
|
||||||
file_path = tmp.name
|
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)
|
reader = csv.reader(f)
|
||||||
headers = next(reader)
|
headers = next(reader)
|
||||||
|
|
||||||
@ -514,7 +519,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
return render(request, "admin/import_mapping.html", context)
|
return render(request, "admin/import_mapping.html", context)
|
||||||
else:
|
else:
|
||||||
form = VoterImportForm()
|
form = VoterImportForm()
|
||||||
|
|
||||||
context = self.admin_site.each_context(request)
|
context = self.admin_site.each_context(request)
|
||||||
context['form'] = form
|
context['form'] = form
|
||||||
context['title'] = "Import Voters"
|
context['title'] = "Import Voters"
|
||||||
@ -554,7 +559,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
for field_name, _ in EVENT_MAPPABLE_FIELDS:
|
for field_name, _ in EVENT_MAPPABLE_FIELDS:
|
||||||
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
||||||
try:
|
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)
|
reader = csv.DictReader(f)
|
||||||
total_count = 0
|
total_count = 0
|
||||||
create_count = 0
|
create_count = 0
|
||||||
@ -564,7 +569,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
total_count += 1
|
total_count += 1
|
||||||
event_name = row.get(mapping.get('name'))
|
event_name = row.get(mapping.get('name'))
|
||||||
event_date = row.get(mapping.get('date'))
|
event_date = row.get(mapping.get('date'))
|
||||||
|
|
||||||
exists = False
|
exists = False
|
||||||
if event_name and event_date:
|
if event_name and event_date:
|
||||||
try:
|
try:
|
||||||
@ -579,18 +584,18 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
|
|
||||||
if dt:
|
if dt:
|
||||||
exists = Event.objects.filter(tenant=tenant, name=event_name, date=dt).exists()
|
exists = Event.objects.filter(tenant=tenant, name=event_name, date=dt).exists()
|
||||||
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# Handle cases where date parsing fails
|
# Handle cases where date parsing fails
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if exists:
|
if exists:
|
||||||
update_count += 1
|
update_count += 1
|
||||||
action = 'update'
|
action = 'update'
|
||||||
else:
|
else:
|
||||||
create_count += 1
|
create_count += 1
|
||||||
action = 'create'
|
action = 'create'
|
||||||
|
|
||||||
if len(preview_data) < 10:
|
if len(preview_data) < 10:
|
||||||
preview_data.append({
|
preview_data.append({
|
||||||
'action': action,
|
'action': action,
|
||||||
@ -613,13 +618,13 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
return render(request, "admin/import_preview.html", context)
|
return render(request, "admin/import_preview.html", context)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
|
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
|
|
||||||
elif "_import" in request.POST:
|
elif "_import" in request.POST:
|
||||||
file_path = request.POST.get('file_path')
|
file_path = request.POST.get('file_path')
|
||||||
tenant_id = request.POST.get('tenant')
|
tenant_id = request.POST.get('tenant')
|
||||||
tenant = Tenant.objects.get(id=tenant_id)
|
tenant = Tenant.objects.get(id=tenant_id)
|
||||||
|
|
||||||
mapping = {}
|
mapping = {}
|
||||||
for field_name, _ in EVENT_MAPPABLE_FIELDS:
|
for field_name, _ in EVENT_MAPPABLE_FIELDS:
|
||||||
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
||||||
@ -628,7 +633,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
count = 0
|
count = 0
|
||||||
errors = 0
|
errors = 0
|
||||||
failed_rows = []
|
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)
|
reader = csv.DictReader(f)
|
||||||
for row in reader:
|
for row in reader:
|
||||||
try:
|
try:
|
||||||
@ -658,7 +663,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
errors += 1
|
errors += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
event_type_obj, _ = EventType.objects.get_or_create(tenant=tenant, name=event_type_name)
|
event_type_obj, _ = EventType.objects.get_or_create(tenant=tenant, name=event_type_name)
|
||||||
|
|
||||||
defaults = {
|
defaults = {
|
||||||
@ -699,7 +704,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
row["Import Error"] = str(e)
|
row["Import Error"] = str(e)
|
||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
errors += 1
|
errors += 1
|
||||||
|
|
||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
self.message_user(request, f"Successfully imported {count} events.")
|
self.message_user(request, f"Successfully imported {count} events.")
|
||||||
@ -708,26 +713,26 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
if errors > 0:
|
if errors > 0:
|
||||||
error_url = reverse("admin:event-download-errors")
|
error_url = reverse("admin:event-download-errors")
|
||||||
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
|
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
|
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
else:
|
else:
|
||||||
form = EventImportForm(request.POST, request.FILES)
|
form = EventImportForm(request.POST, request.FILES)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
csv_file = request.FILES['file']
|
csv_file = request.FILES['file']
|
||||||
tenant = form.cleaned_data['tenant']
|
tenant = form.cleaned_data['tenant']
|
||||||
|
|
||||||
if not csv_file.name.endswith('.csv'):
|
if not csv_file.name.endswith('.csv'):
|
||||||
self.message_user(request, "Please upload a CSV file.", level=messages.ERROR)
|
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:
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp:
|
||||||
for chunk in csv_file.chunks():
|
for chunk in csv_file.chunks():
|
||||||
tmp.write(chunk)
|
tmp.write(chunk)
|
||||||
file_path = tmp.name
|
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)
|
reader = csv.reader(f)
|
||||||
headers = next(reader)
|
headers = next(reader)
|
||||||
|
|
||||||
@ -744,7 +749,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
return render(request, "admin/import_mapping.html", context)
|
return render(request, "admin/import_mapping.html", context)
|
||||||
else:
|
else:
|
||||||
form = EventImportForm()
|
form = EventImportForm()
|
||||||
|
|
||||||
context = self.admin_site.each_context(request)
|
context = self.admin_site.each_context(request)
|
||||||
context['form'] = form
|
context['form'] = form
|
||||||
context['title'] = "Import Events"
|
context['title'] = "Import Events"
|
||||||
@ -784,7 +789,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
||||||
|
|
||||||
try:
|
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)
|
reader = csv.DictReader(f)
|
||||||
total_count = 0
|
total_count = 0
|
||||||
create_count = 0
|
create_count = 0
|
||||||
@ -797,14 +802,14 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
exists = False
|
exists = False
|
||||||
if email:
|
if email:
|
||||||
exists = Volunteer.objects.filter(tenant=tenant, email=email).exists()
|
exists = Volunteer.objects.filter(tenant=tenant, email=email).exists()
|
||||||
|
|
||||||
if exists:
|
if exists:
|
||||||
update_count += 1
|
update_count += 1
|
||||||
action = 'update'
|
action = 'update'
|
||||||
else:
|
else:
|
||||||
create_count += 1
|
create_count += 1
|
||||||
action = 'create'
|
action = 'create'
|
||||||
|
|
||||||
if len(preview_data) < 10:
|
if len(preview_data) < 10:
|
||||||
preview_data.append({
|
preview_data.append({
|
||||||
'action': action,
|
'action': action,
|
||||||
@ -827,13 +832,13 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
return render(request, "admin/import_preview.html", context)
|
return render(request, "admin/import_preview.html", context)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
|
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
|
|
||||||
elif "_import" in request.POST:
|
elif "_import" in request.POST:
|
||||||
file_path = request.POST.get('file_path')
|
file_path = request.POST.get('file_path')
|
||||||
tenant_id = request.POST.get('tenant')
|
tenant_id = request.POST.get('tenant')
|
||||||
tenant = Tenant.objects.get(id=tenant_id)
|
tenant = Tenant.objects.get(id=tenant_id)
|
||||||
|
|
||||||
mapping = {}
|
mapping = {}
|
||||||
for field_name, _ in VOLUNTEER_MAPPABLE_FIELDS:
|
for field_name, _ in VOLUNTEER_MAPPABLE_FIELDS:
|
||||||
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
||||||
@ -842,7 +847,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
count = 0
|
count = 0
|
||||||
errors = 0
|
errors = 0
|
||||||
failed_rows = []
|
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)
|
reader = csv.DictReader(f)
|
||||||
for row in reader:
|
for row in reader:
|
||||||
try:
|
try:
|
||||||
@ -852,7 +857,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
errors += 1
|
errors += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
defaults = {
|
defaults = {
|
||||||
'first_name': row.get(mapping.get('first_name')) or '',
|
'first_name': row.get(mapping.get('first_name')) or '',
|
||||||
'last_name': row.get(mapping.get('last_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)
|
row["Import Error"] = str(e)
|
||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
errors += 1
|
errors += 1
|
||||||
|
|
||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
self.message_user(request, f"Successfully imported {count} volunteers.")
|
self.message_user(request, f"Successfully imported {count} volunteers.")
|
||||||
@ -880,26 +885,26 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
if errors > 0:
|
if errors > 0:
|
||||||
error_url = reverse("admin:volunteer-download-errors")
|
error_url = reverse("admin:volunteer-download-errors")
|
||||||
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
|
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
|
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
else:
|
else:
|
||||||
form = VolunteerImportForm(request.POST, request.FILES)
|
form = VolunteerImportForm(request.POST, request.FILES)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
csv_file = request.FILES['file']
|
csv_file = request.FILES['file']
|
||||||
tenant = form.cleaned_data['tenant']
|
tenant = form.cleaned_data['tenant']
|
||||||
|
|
||||||
if not csv_file.name.endswith('.csv'):
|
if not csv_file.name.endswith('.csv'):
|
||||||
self.message_user(request, "Please upload a CSV file.", level=messages.ERROR)
|
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:
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp:
|
||||||
for chunk in csv_file.chunks():
|
for chunk in csv_file.chunks():
|
||||||
tmp.write(chunk)
|
tmp.write(chunk)
|
||||||
file_path = tmp.name
|
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)
|
reader = csv.reader(f)
|
||||||
headers = next(reader)
|
headers = next(reader)
|
||||||
|
|
||||||
@ -916,7 +921,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
return render(request, "admin/import_mapping.html", context)
|
return render(request, "admin/import_mapping.html", context)
|
||||||
else:
|
else:
|
||||||
form = VolunteerImportForm()
|
form = VolunteerImportForm()
|
||||||
|
|
||||||
context = self.admin_site.each_context(request)
|
context = self.admin_site.each_context(request)
|
||||||
context['form'] = form
|
context['form'] = form
|
||||||
context['title'] = "Import Volunteers"
|
context['title'] = "Import Volunteers"
|
||||||
@ -935,7 +940,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
extra_context = extra_context or {}
|
extra_context = extra_context or {}
|
||||||
from core.models import Tenant
|
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)
|
return super().changelist_list(request, extra_context=extra_context)
|
||||||
|
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
urls = super().get_urls()
|
urls = super().get_urls()
|
||||||
@ -955,7 +960,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
for field_name, _ in EVENT_PARTICIPATION_MAPPABLE_FIELDS:
|
for field_name, _ in EVENT_PARTICIPATION_MAPPABLE_FIELDS:
|
||||||
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
||||||
try:
|
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)
|
reader = csv.DictReader(f)
|
||||||
total_count = 0
|
total_count = 0
|
||||||
create_count = 0
|
create_count = 0
|
||||||
@ -964,8 +969,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
for row in reader:
|
for row in reader:
|
||||||
total_count += 1
|
total_count += 1
|
||||||
voter_id = row.get(mapping.get('voter_id'))
|
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
|
# Extract first_name and last_name from CSV based on mapping
|
||||||
csv_first_name = row.get(mapping.get('first_name'), '')
|
csv_first_name = row.get(mapping.get('first_name'), '')
|
||||||
csv_last_name = row.get(mapping.get('last_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()
|
exists = EventParticipation.objects.filter(voter=voter, event__name=event_name).exists()
|
||||||
except Voter.DoesNotExist:
|
except Voter.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if exists:
|
if exists:
|
||||||
update_count += 1
|
update_count += 1
|
||||||
action = 'update'
|
action = 'update'
|
||||||
else:
|
else:
|
||||||
create_count += 1
|
create_count += 1
|
||||||
action = 'create'
|
action = 'create'
|
||||||
|
|
||||||
if len(preview_data) < 10:
|
if len(preview_data) < 10:
|
||||||
preview_data.append({
|
preview_data.append({
|
||||||
'action': action,
|
'action': action,
|
||||||
@ -1012,19 +1016,19 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
return render(request, "admin/import_preview.html", context)
|
return render(request, "admin/import_preview.html", context)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
|
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
|
|
||||||
elif "_import" in request.POST:
|
elif "_import" in request.POST:
|
||||||
file_path = request.POST.get('file_path')
|
file_path = request.POST.get('file_path')
|
||||||
tenant_id = request.POST.get('tenant')
|
tenant_id = request.POST.get('tenant')
|
||||||
tenant = Tenant.objects.get(id=tenant_id)
|
tenant = Tenant.objects.get(id=tenant_id)
|
||||||
|
|
||||||
mapping = {}
|
mapping = {}
|
||||||
for field_name, _ in EVENT_PARTICIPATION_MAPPABLE_FIELDS:
|
for field_name, _ in EVENT_PARTICIPATION_MAPPABLE_FIELDS:
|
||||||
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
||||||
|
|
||||||
try:
|
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)
|
reader = csv.DictReader(f)
|
||||||
count = 0
|
count = 0
|
||||||
errors = 0
|
errors = 0
|
||||||
@ -1033,13 +1037,16 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
try:
|
try:
|
||||||
voter_id = row.get(mapping.get('voter_id')) if mapping.get('voter_id') else None
|
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
|
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:
|
if not voter_id:
|
||||||
row["Import Error"] = "Missing voter ID"
|
row["Import Error"] = "Missing voter ID"
|
||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
errors += 1
|
errors += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
voter = Voter.objects.get(tenant=tenant, voter_id=voter_id)
|
voter = Voter.objects.get(tenant=tenant, voter_id=voter_id)
|
||||||
except Voter.DoesNotExist:
|
except Voter.DoesNotExist:
|
||||||
@ -1085,7 +1092,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
row["Import Error"] = str(e)
|
row["Import Error"] = str(e)
|
||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
errors += 1
|
errors += 1
|
||||||
|
|
||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
self.message_user(request, f"Successfully imported {count} participations.")
|
self.message_user(request, f"Successfully imported {count} participations.")
|
||||||
@ -1096,26 +1103,26 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
if errors > 0:
|
if errors > 0:
|
||||||
error_url = reverse("admin:eventparticipation-download-errors")
|
error_url = reverse("admin:eventparticipation-download-errors")
|
||||||
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
|
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
|
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
else:
|
else:
|
||||||
form = EventParticipationImportForm(request.POST, request.FILES)
|
form = EventParticipationImportForm(request.POST, request.FILES)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
csv_file = request.FILES['file']
|
csv_file = request.FILES['file']
|
||||||
tenant = form.cleaned_data['tenant']
|
tenant = form.cleaned_data['tenant']
|
||||||
|
|
||||||
if not csv_file.name.endswith('.csv'):
|
if not csv_file.name.endswith('.csv'):
|
||||||
self.message_user(request, "Please upload a CSV file.", level=messages.ERROR)
|
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:
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp:
|
||||||
for chunk in csv_file.chunks():
|
for chunk in csv_file.chunks():
|
||||||
tmp.write(chunk)
|
tmp.write(chunk)
|
||||||
file_path = tmp.name
|
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)
|
reader = csv.reader(f)
|
||||||
headers = next(reader)
|
headers = next(reader)
|
||||||
|
|
||||||
@ -1132,7 +1139,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
return render(request, "admin/import_mapping.html", context)
|
return render(request, "admin/import_mapping.html", context)
|
||||||
else:
|
else:
|
||||||
form = EventParticipationImportForm()
|
form = EventParticipationImportForm()
|
||||||
|
|
||||||
context = self.admin_site.each_context(request)
|
context = self.admin_site.each_context(request)
|
||||||
context['form'] = form
|
context['form'] = form
|
||||||
context['title'] = "Import Participations"
|
context['title'] = "Import Participations"
|
||||||
@ -1170,7 +1177,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
for field_name, _ in DONATION_MAPPABLE_FIELDS:
|
for field_name, _ in DONATION_MAPPABLE_FIELDS:
|
||||||
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
||||||
try:
|
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)
|
reader = csv.DictReader(f)
|
||||||
total_count = 0
|
total_count = 0
|
||||||
create_count = 0
|
create_count = 0
|
||||||
@ -1179,18 +1186,18 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
for row in reader:
|
for row in reader:
|
||||||
total_count += 1
|
total_count += 1
|
||||||
voter_id = row.get(mapping.get('voter_id'))
|
voter_id = row.get(mapping.get('voter_id'))
|
||||||
|
|
||||||
exists = False
|
exists = False
|
||||||
if voter_id:
|
if voter_id:
|
||||||
exists = Voter.objects.filter(tenant=tenant, voter_id=voter_id).exists()
|
exists = Voter.objects.filter(tenant=tenant, voter_id=voter_id).exists()
|
||||||
|
|
||||||
if exists:
|
if exists:
|
||||||
update_count += 1
|
update_count += 1
|
||||||
action = 'update'
|
action = 'update'
|
||||||
else:
|
else:
|
||||||
create_count += 1
|
create_count += 1
|
||||||
action = 'create'
|
action = 'create'
|
||||||
|
|
||||||
if len(preview_data) < 10:
|
if len(preview_data) < 10:
|
||||||
preview_data.append({
|
preview_data.append({
|
||||||
'action': action,
|
'action': action,
|
||||||
@ -1213,13 +1220,13 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
return render(request, "admin/import_preview.html", context)
|
return render(request, "admin/import_preview.html", context)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
|
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
|
|
||||||
elif "_import" in request.POST:
|
elif "_import" in request.POST:
|
||||||
file_path = request.POST.get('file_path')
|
file_path = request.POST.get('file_path')
|
||||||
tenant_id = request.POST.get('tenant')
|
tenant_id = request.POST.get('tenant')
|
||||||
tenant = Tenant.objects.get(id=tenant_id)
|
tenant = Tenant.objects.get(id=tenant_id)
|
||||||
|
|
||||||
mapping = {}
|
mapping = {}
|
||||||
for field_name, _ in DONATION_MAPPABLE_FIELDS:
|
for field_name, _ in DONATION_MAPPABLE_FIELDS:
|
||||||
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
||||||
@ -1228,7 +1235,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
count = 0
|
count = 0
|
||||||
errors = 0
|
errors = 0
|
||||||
failed_rows = []
|
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)
|
reader = csv.DictReader(f)
|
||||||
for row in reader:
|
for row in reader:
|
||||||
try:
|
try:
|
||||||
@ -1237,6 +1244,9 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
amount_str = row.get(mapping.get('amount'))
|
amount_str = row.get(mapping.get('amount'))
|
||||||
method_name = row.get(mapping.get('method'))
|
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:
|
if not voter_id:
|
||||||
row["Import Error"] = "Missing voter ID"
|
row["Import Error"] = "Missing voter ID"
|
||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
@ -1248,7 +1258,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
errors += 1
|
errors += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
voter = Voter.objects.get(tenant=tenant, voter_id=voter_id)
|
voter = Voter.objects.get(tenant=tenant, voter_id=voter_id)
|
||||||
except Voter.DoesNotExist:
|
except Voter.DoesNotExist:
|
||||||
@ -1256,7 +1266,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
errors += 1
|
errors += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if '/' in date_str:
|
if '/' in date_str:
|
||||||
parsed_date = datetime.strptime(date_str, '%m/%d/%Y').date()
|
parsed_date = datetime.strptime(date_str, '%m/%d/%Y').date()
|
||||||
@ -1280,7 +1290,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
errors += 1
|
errors += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
donation_method, _ = DonationMethod.objects.get_or_create(tenant=tenant, name=method_name)
|
donation_method, _ = DonationMethod.objects.get_or_create(tenant=tenant, name=method_name)
|
||||||
|
|
||||||
Donation.objects.create(
|
Donation.objects.create(
|
||||||
@ -1295,7 +1305,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
row["Import Error"] = str(e)
|
row["Import Error"] = str(e)
|
||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
errors += 1
|
errors += 1
|
||||||
|
|
||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
self.message_user(request, f"Successfully imported {count} donations.")
|
self.message_user(request, f"Successfully imported {count} donations.")
|
||||||
@ -1304,26 +1314,26 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
if errors > 0:
|
if errors > 0:
|
||||||
error_url = reverse("admin:donation-download-errors")
|
error_url = reverse("admin:donation-download-errors")
|
||||||
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
|
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
|
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
else:
|
else:
|
||||||
form = DonationImportForm(request.POST, request.FILES)
|
form = DonationImportForm(request.POST, request.FILES)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
csv_file = request.FILES['file']
|
csv_file = request.FILES['file']
|
||||||
tenant = form.cleaned_data['tenant']
|
tenant = form.cleaned_data['tenant']
|
||||||
|
|
||||||
if not csv_file.name.endswith('.csv'):
|
if not csv_file.name.endswith('.csv'):
|
||||||
self.message_user(request, "Please upload a CSV file.", level=messages.ERROR)
|
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:
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp:
|
||||||
for chunk in csv_file.chunks():
|
for chunk in csv_file.chunks():
|
||||||
tmp.write(chunk)
|
tmp.write(chunk)
|
||||||
file_path = tmp.name
|
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)
|
reader = csv.reader(f)
|
||||||
headers = next(reader)
|
headers = next(reader)
|
||||||
|
|
||||||
@ -1340,7 +1350,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
return render(request, "admin/import_mapping.html", context)
|
return render(request, "admin/import_mapping.html", context)
|
||||||
else:
|
else:
|
||||||
form = DonationImportForm()
|
form = DonationImportForm()
|
||||||
|
|
||||||
context = self.admin_site.each_context(request)
|
context = self.admin_site.each_context(request)
|
||||||
context['form'] = form
|
context['form'] = form
|
||||||
context['title'] = "Import Donations"
|
context['title'] = "Import Donations"
|
||||||
@ -1379,7 +1389,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
for field_name, _ in INTERACTION_MAPPABLE_FIELDS:
|
for field_name, _ in INTERACTION_MAPPABLE_FIELDS:
|
||||||
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
||||||
try:
|
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)
|
reader = csv.DictReader(f)
|
||||||
total_count = 0
|
total_count = 0
|
||||||
create_count = 0
|
create_count = 0
|
||||||
@ -1389,18 +1399,18 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
total_count += 1
|
total_count += 1
|
||||||
voter_id = row.get(mapping.get('voter_id'))
|
voter_id = row.get(mapping.get('voter_id'))
|
||||||
volunteer_email = row.get(mapping.get('volunteer_email'))
|
volunteer_email = row.get(mapping.get('volunteer_email'))
|
||||||
|
|
||||||
exists = False
|
exists = False
|
||||||
if voter_id:
|
if voter_id:
|
||||||
exists = Voter.objects.filter(tenant=tenant, voter_id=voter_id).exists()
|
exists = Voter.objects.filter(tenant=tenant, voter_id=voter_id).exists()
|
||||||
|
|
||||||
if exists:
|
if exists:
|
||||||
update_count += 1
|
update_count += 1
|
||||||
action = 'update'
|
action = 'update'
|
||||||
else:
|
else:
|
||||||
create_count += 1
|
create_count += 1
|
||||||
action = 'create'
|
action = 'create'
|
||||||
|
|
||||||
if len(preview_data) < 10:
|
if len(preview_data) < 10:
|
||||||
preview_data.append({
|
preview_data.append({
|
||||||
'action': action,
|
'action': action,
|
||||||
@ -1423,13 +1433,13 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
return render(request, "admin/import_preview.html", context)
|
return render(request, "admin/import_preview.html", context)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
|
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
|
|
||||||
elif "_import" in request.POST:
|
elif "_import" in request.POST:
|
||||||
file_path = request.POST.get('file_path')
|
file_path = request.POST.get('file_path')
|
||||||
tenant_id = request.POST.get('tenant')
|
tenant_id = request.POST.get('tenant')
|
||||||
tenant = Tenant.objects.get(id=tenant_id)
|
tenant = Tenant.objects.get(id=tenant_id)
|
||||||
|
|
||||||
mapping = {}
|
mapping = {}
|
||||||
for field_name, _ in INTERACTION_MAPPABLE_FIELDS:
|
for field_name, _ in INTERACTION_MAPPABLE_FIELDS:
|
||||||
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
||||||
@ -1438,7 +1448,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
count = 0
|
count = 0
|
||||||
errors = 0
|
errors = 0
|
||||||
failed_rows = []
|
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)
|
reader = csv.DictReader(f)
|
||||||
for row in reader:
|
for row in reader:
|
||||||
try:
|
try:
|
||||||
@ -1447,6 +1457,9 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
date_str = row.get(mapping.get('date'))
|
date_str = row.get(mapping.get('date'))
|
||||||
type_name = row.get(mapping.get('type'))
|
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:
|
if not voter_id:
|
||||||
row["Import Error"] = "Missing voter ID"
|
row["Import Error"] = "Missing voter ID"
|
||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
@ -1458,7 +1471,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
errors += 1
|
errors += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
voter = Voter.objects.get(tenant=tenant, voter_id=voter_id)
|
voter = Voter.objects.get(tenant=tenant, voter_id=voter_id)
|
||||||
except Voter.DoesNotExist:
|
except Voter.DoesNotExist:
|
||||||
@ -1466,7 +1479,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
errors += 1
|
errors += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
volunteer = None
|
volunteer = None
|
||||||
if volunteer_email:
|
if volunteer_email:
|
||||||
try:
|
try:
|
||||||
@ -1506,7 +1519,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
row["Import Error"] = str(e)
|
row["Import Error"] = str(e)
|
||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
errors += 1
|
errors += 1
|
||||||
|
|
||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
self.message_user(request, f"Successfully imported {count} interactions.")
|
self.message_user(request, f"Successfully imported {count} interactions.")
|
||||||
@ -1515,26 +1528,26 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
if errors > 0:
|
if errors > 0:
|
||||||
error_url = reverse("admin:interaction-download-errors")
|
error_url = reverse("admin:interaction-download-errors")
|
||||||
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
|
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
|
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
else:
|
else:
|
||||||
form = InteractionImportForm(request.POST, request.FILES)
|
form = InteractionImportForm(request.POST, request.FILES)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
csv_file = request.FILES['file']
|
csv_file = request.FILES['file']
|
||||||
tenant = form.cleaned_data['tenant']
|
tenant = form.cleaned_data['tenant']
|
||||||
|
|
||||||
if not csv_file.name.endswith('.csv'):
|
if not csv_file.name.endswith('.csv'):
|
||||||
self.message_user(request, "Please upload a CSV file.", level=messages.ERROR)
|
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:
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp:
|
||||||
for chunk in csv_file.chunks():
|
for chunk in csv_file.chunks():
|
||||||
tmp.write(chunk)
|
tmp.write(chunk)
|
||||||
file_path = tmp.name
|
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)
|
reader = csv.reader(f)
|
||||||
headers = next(reader)
|
headers = next(reader)
|
||||||
|
|
||||||
@ -1551,7 +1564,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
return render(request, "admin/import_mapping.html", context)
|
return render(request, "admin/import_mapping.html", context)
|
||||||
else:
|
else:
|
||||||
form = InteractionImportForm()
|
form = InteractionImportForm()
|
||||||
|
|
||||||
context = self.admin_site.each_context(request)
|
context = self.admin_site.each_context(request)
|
||||||
context['form'] = form
|
context['form'] = form
|
||||||
context['title'] = "Import Interactions"
|
context['title'] = "Import Interactions"
|
||||||
@ -1589,7 +1602,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
for field_name, _ in VOTER_LIKELIHOOD_MAPPABLE_FIELDS:
|
for field_name, _ in VOTER_LIKELIHOOD_MAPPABLE_FIELDS:
|
||||||
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
||||||
try:
|
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)
|
reader = csv.DictReader(f)
|
||||||
total_count = 0
|
total_count = 0
|
||||||
create_count = 0
|
create_count = 0
|
||||||
@ -1598,18 +1611,18 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
for row in reader:
|
for row in reader:
|
||||||
total_count += 1
|
total_count += 1
|
||||||
voter_id = row.get(mapping.get('voter_id'))
|
voter_id = row.get(mapping.get('voter_id'))
|
||||||
|
|
||||||
exists = False
|
exists = False
|
||||||
if voter_id:
|
if voter_id:
|
||||||
exists = Voter.objects.filter(tenant=tenant, voter_id=voter_id).exists()
|
exists = Voter.objects.filter(tenant=tenant, voter_id=voter_id).exists()
|
||||||
|
|
||||||
if exists:
|
if exists:
|
||||||
update_count += 1
|
update_count += 1
|
||||||
action = 'update'
|
action = 'update'
|
||||||
else:
|
else:
|
||||||
create_count += 1
|
create_count += 1
|
||||||
action = 'create'
|
action = 'create'
|
||||||
|
|
||||||
if len(preview_data) < 10:
|
if len(preview_data) < 10:
|
||||||
preview_data.append({
|
preview_data.append({
|
||||||
'action': action,
|
'action': action,
|
||||||
@ -1632,13 +1645,13 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
return render(request, "admin/import_preview.html", context)
|
return render(request, "admin/import_preview.html", context)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
|
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
|
|
||||||
elif "_import" in request.POST:
|
elif "_import" in request.POST:
|
||||||
file_path = request.POST.get('file_path')
|
file_path = request.POST.get('file_path')
|
||||||
tenant_id = request.POST.get('tenant')
|
tenant_id = request.POST.get('tenant')
|
||||||
tenant = Tenant.objects.get(id=tenant_id)
|
tenant = Tenant.objects.get(id=tenant_id)
|
||||||
|
|
||||||
mapping = {}
|
mapping = {}
|
||||||
for field_name, _ in VOTER_LIKELIHOOD_MAPPABLE_FIELDS:
|
for field_name, _ in VOTER_LIKELIHOOD_MAPPABLE_FIELDS:
|
||||||
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
||||||
@ -1647,7 +1660,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
count = 0
|
count = 0
|
||||||
errors = 0
|
errors = 0
|
||||||
failed_rows = []
|
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)
|
reader = csv.DictReader(f)
|
||||||
for row in reader:
|
for row in reader:
|
||||||
try:
|
try:
|
||||||
@ -1655,12 +1668,15 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
election_type_name = row.get(mapping.get('election_type'))
|
election_type_name = row.get(mapping.get('election_type'))
|
||||||
likelihood_val = row.get(mapping.get('likelihood'))
|
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:
|
if not voter_id:
|
||||||
row["Import Error"] = "Missing voter ID"
|
row["Import Error"] = "Missing voter ID"
|
||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
errors += 1
|
errors += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not election_type_name or not likelihood_val:
|
if not election_type_name or not likelihood_val:
|
||||||
row["Import Error"] = "Missing election type or likelihood"
|
row["Import Error"] = "Missing election type or likelihood"
|
||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
@ -1674,7 +1690,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
errors += 1
|
errors += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
election_type, _ = ElectionType.objects.get_or_create(tenant=tenant, name=election_type_name)
|
election_type, _ = ElectionType.objects.get_or_create(tenant=tenant, name=election_type_name)
|
||||||
|
|
||||||
VoterLikelihood.objects.update_or_create(
|
VoterLikelihood.objects.update_or_create(
|
||||||
@ -1688,7 +1704,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
row["Import Error"] = str(e)
|
row["Import Error"] = str(e)
|
||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
errors += 1
|
errors += 1
|
||||||
|
|
||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
self.message_user(request, f"Import complete: {count} likelihoods created/updated.")
|
self.message_user(request, f"Import complete: {count} likelihoods created/updated.")
|
||||||
@ -1697,26 +1713,26 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
if errors > 0:
|
if errors > 0:
|
||||||
error_url = reverse("admin:voterlikelihood-download-errors")
|
error_url = reverse("admin:voterlikelihood-download-errors")
|
||||||
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
|
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
|
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
else:
|
else:
|
||||||
form = VoterLikelihoodImportForm(request.POST, request.FILES)
|
form = VoterLikelihoodImportForm(request.POST, request.FILES)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
csv_file = request.FILES['file']
|
csv_file = request.FILES['file']
|
||||||
tenant = form.cleaned_data['tenant']
|
tenant = form.cleaned_data['tenant']
|
||||||
|
|
||||||
if not csv_file.name.endswith('.csv'):
|
if not csv_file.name.endswith('.csv'):
|
||||||
self.message_user(request, "Please upload a CSV file.", level=messages.ERROR)
|
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:
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp:
|
||||||
for chunk in csv_file.chunks():
|
for chunk in csv_file.chunks():
|
||||||
tmp.write(chunk)
|
tmp.write(chunk)
|
||||||
file_path = tmp.name
|
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)
|
reader = csv.reader(f)
|
||||||
headers = next(reader)
|
headers = next(reader)
|
||||||
|
|
||||||
@ -1733,7 +1749,7 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
return render(request, "admin/import_mapping.html", context)
|
return render(request, "admin/import_mapping.html", context)
|
||||||
else:
|
else:
|
||||||
form = VoterLikelihoodImportForm()
|
form = VoterLikelihoodImportForm()
|
||||||
|
|
||||||
context = self.admin_site.each_context(request)
|
context = self.admin_site.each_context(request)
|
||||||
context['form'] = form
|
context['form'] = form
|
||||||
context['title'] = "Import Likelihoods"
|
context['title'] = "Import Likelihoods"
|
||||||
@ -1772,7 +1788,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
for field_name, _ in VOTING_RECORD_MAPPABLE_FIELDS:
|
for field_name, _ in VOTING_RECORD_MAPPABLE_FIELDS:
|
||||||
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
||||||
try:
|
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)
|
reader = csv.DictReader(f)
|
||||||
total_count = 0
|
total_count = 0
|
||||||
create_count = 0
|
create_count = 0
|
||||||
@ -1782,7 +1798,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
total_count += 1
|
total_count += 1
|
||||||
voter_id = row.get(mapping.get('voter_id'))
|
voter_id = row.get(mapping.get('voter_id'))
|
||||||
election_date = row.get(mapping.get('election_date'))
|
election_date = row.get(mapping.get('election_date'))
|
||||||
|
|
||||||
exists = False
|
exists = False
|
||||||
if voter_id and election_date:
|
if voter_id and election_date:
|
||||||
try:
|
try:
|
||||||
@ -1797,18 +1813,18 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
|
|
||||||
if dt:
|
if dt:
|
||||||
exists = VotingRecord.objects.filter(voter__tenant=tenant, voter__voter_id=voter_id, election_date=dt).exists()
|
exists = VotingRecord.objects.filter(voter__tenant=tenant, voter__voter_id=voter_id, election_date=dt).exists()
|
||||||
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# Handle cases where date parsing fails
|
# Handle cases where date parsing fails
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if exists:
|
if exists:
|
||||||
update_count += 1
|
update_count += 1
|
||||||
action = 'update'
|
action = 'update'
|
||||||
else:
|
else:
|
||||||
create_count += 1
|
create_count += 1
|
||||||
action = 'create'
|
action = 'create'
|
||||||
|
|
||||||
if len(preview_data) < 10:
|
if len(preview_data) < 10:
|
||||||
preview_data.append({
|
preview_data.append({
|
||||||
'action': action,
|
'action': action,
|
||||||
@ -1831,13 +1847,13 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
return render(request, "admin/import_preview.html", context)
|
return render(request, "admin/import_preview.html", context)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
|
self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
|
|
||||||
elif "_import" in request.POST:
|
elif "_import" in request.POST:
|
||||||
file_path = request.POST.get('file_path')
|
file_path = request.POST.get('file_path')
|
||||||
tenant_id = request.POST.get('tenant')
|
tenant_id = request.POST.get('tenant')
|
||||||
tenant = Tenant.objects.get(id=tenant_id)
|
tenant = Tenant.objects.get(id=tenant_id)
|
||||||
|
|
||||||
mapping = {}
|
mapping = {}
|
||||||
for field_name, _ in VOTING_RECORD_MAPPABLE_FIELDS:
|
for field_name, _ in VOTING_RECORD_MAPPABLE_FIELDS:
|
||||||
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
mapping[field_name] = request.POST.get(f'map_{field_name}')
|
||||||
@ -1846,7 +1862,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
count = 0
|
count = 0
|
||||||
errors = 0
|
errors = 0
|
||||||
failed_rows = []
|
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)
|
reader = csv.DictReader(f)
|
||||||
for row in reader:
|
for row in reader:
|
||||||
try:
|
try:
|
||||||
@ -1855,6 +1871,9 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
election_description = row.get(mapping.get('election_description'))
|
election_description = row.get(mapping.get('election_description'))
|
||||||
primary_party = row.get(mapping.get('primary_party'))
|
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:
|
if not voter_id:
|
||||||
row["Import Error"] = "Missing voter ID"
|
row["Import Error"] = "Missing voter ID"
|
||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
@ -1866,7 +1885,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
errors += 1
|
errors += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
voter = Voter.objects.get(tenant=tenant, voter_id=voter_id)
|
voter = Voter.objects.get(tenant=tenant, voter_id=voter_id)
|
||||||
except Voter.DoesNotExist:
|
except Voter.DoesNotExist:
|
||||||
@ -1905,7 +1924,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
row["Import Error"] = str(e)
|
row["Import Error"] = str(e)
|
||||||
failed_rows.append(row)
|
failed_rows.append(row)
|
||||||
errors += 1
|
errors += 1
|
||||||
|
|
||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
self.message_user(request, f"Successfully imported {count} voting records.")
|
self.message_user(request, f"Successfully imported {count} voting records.")
|
||||||
@ -1914,26 +1933,26 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
if errors > 0:
|
if errors > 0:
|
||||||
error_url = reverse("admin:votingrecord-download-errors")
|
error_url = reverse("admin:votingrecord-download-errors")
|
||||||
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
|
self.message_user(request, mark_safe(f"Failed to import {errors} rows. <a href='{error_url}' download>Download failed records</a>"), level=messages.WARNING)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
|
self.message_user(request, f"Error processing file: {e}", level=messages.ERROR)
|
||||||
return redirect("..\\n")
|
return redirect("../")
|
||||||
else:
|
else:
|
||||||
form = VotingRecordImportForm(request.POST, request.FILES)
|
form = VotingRecordImportForm(request.POST, request.FILES)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
csv_file = request.FILES['file']
|
csv_file = request.FILES['file']
|
||||||
tenant = form.cleaned_data['tenant']
|
tenant = form.cleaned_data['tenant']
|
||||||
|
|
||||||
if not csv_file.name.endswith('.csv'):
|
if not csv_file.name.endswith('.csv'):
|
||||||
self.message_user(request, "Please upload a CSV file.", level=messages.ERROR)
|
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:
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp:
|
||||||
for chunk in csv_file.chunks():
|
for chunk in csv_file.chunks():
|
||||||
tmp.write(chunk)
|
tmp.write(chunk)
|
||||||
file_path = tmp.name
|
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)
|
reader = csv.reader(f)
|
||||||
headers = next(reader)
|
headers = next(reader)
|
||||||
|
|
||||||
@ -1950,7 +1969,7 @@ class VotingRecordAdmin(BaseImportAdminMixin, admin.ModelAdmin):
|
|||||||
return render(request, "admin/import_mapping.html", context)
|
return render(request, "admin/import_mapping.html", context)
|
||||||
else:
|
else:
|
||||||
form = VotingRecordImportForm()
|
form = VotingRecordImportForm()
|
||||||
|
|
||||||
context = self.admin_site.each_context(request)
|
context = self.admin_site.each_context(request)
|
||||||
context['form'] = form
|
context['form'] = form
|
||||||
context['title'] = "Import Voting Records"
|
context['title'] = "Import Voting Records"
|
||||||
|
|||||||
@ -118,6 +118,7 @@ class AdvancedVoterSearchForm(forms.Form):
|
|||||||
neighborhood = forms.CharField(required=False)
|
neighborhood = forms.CharField(required=False)
|
||||||
district = forms.CharField(required=False)
|
district = forms.CharField(required=False)
|
||||||
precinct = forms.CharField(required=False)
|
precinct = forms.CharField(required=False)
|
||||||
|
email = forms.EmailField(required=False) # Added email field
|
||||||
phone_type = forms.ChoiceField(
|
phone_type = forms.ChoiceField(
|
||||||
choices=[('', 'Any')] + Voter.PHONE_TYPE_CHOICES,
|
choices=[('', 'Any')] + Voter.PHONE_TYPE_CHOICES,
|
||||||
required=False
|
required=False
|
||||||
@ -125,7 +126,7 @@ class AdvancedVoterSearchForm(forms.Form):
|
|||||||
is_targeted = forms.BooleanField(required=False, label="Targeted Only")
|
is_targeted = forms.BooleanField(required=False, label="Targeted Only")
|
||||||
door_visit = forms.BooleanField(required=False, label="Visited Only")
|
door_visit = forms.BooleanField(required=False, label="Visited Only")
|
||||||
candidate_support = forms.ChoiceField(
|
candidate_support = forms.ChoiceField(
|
||||||
choices=[('', 'Any')] + Voter.SUPPORT_CHOICES,
|
choices=[('', 'Any')] + Voter.CANDIDATE_SUPPORT_CHOICES,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
yard_sign = forms.ChoiceField(
|
yard_sign = forms.ChoiceField(
|
||||||
@ -443,7 +444,7 @@ class DoorVisitLogForm(forms.Form):
|
|||||||
label="Wants a Yard Sign"
|
label="Wants a Yard Sign"
|
||||||
)
|
)
|
||||||
candidate_support = forms.ChoiceField(
|
candidate_support = forms.ChoiceField(
|
||||||
choices=Voter.SUPPORT_CHOICES,
|
choices=Voter.CANDIDATE_SUPPORT_CHOICES,
|
||||||
initial="unknown",
|
initial="unknown",
|
||||||
widget=forms.Select(attrs={"class": "form-select"}),
|
widget=forms.Select(attrs={"class": "form-select"}),
|
||||||
label="Candidate Support"
|
label="Candidate Support"
|
||||||
@ -484,16 +485,6 @@ class UserUpdateForm(forms.ModelForm):
|
|||||||
model = User
|
model = User
|
||||||
fields = ['first_name', 'last_name', 'email']
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
for field in self.fields.values():
|
for field in self.fields.values():
|
||||||
|
|||||||
@ -125,7 +125,7 @@ class Interest(models.Model):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
class Voter(models.Model):
|
class Voter(models.Model):
|
||||||
SUPPORT_CHOICES = [
|
CANDIDATE_SUPPORT_CHOICES = [
|
||||||
('unknown', 'Unknown'),
|
('unknown', 'Unknown'),
|
||||||
('supporting', 'Supporting'),
|
('supporting', 'Supporting'),
|
||||||
('not_supporting', 'Not Supporting'),
|
('not_supporting', 'Not Supporting'),
|
||||||
@ -170,7 +170,7 @@ class Voter(models.Model):
|
|||||||
precinct = models.CharField(max_length=100, blank=True, db_index=True)
|
precinct = models.CharField(max_length=100, blank=True, db_index=True)
|
||||||
registration_date = models.DateField(null=True, blank=True)
|
registration_date = models.DateField(null=True, blank=True)
|
||||||
is_targeted = models.BooleanField(default=False, db_index=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)
|
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)
|
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)
|
notes = models.TextField(blank=True)
|
||||||
|
|||||||
@ -49,6 +49,10 @@
|
|||||||
<label class="form-label small fw-bold text-muted">Precinct</label>
|
<label class="form-label small fw-bold text-muted">Precinct</label>
|
||||||
{{ form.precinct }}
|
{{ form.precinct }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small fw-bold text-muted">Email</label>
|
||||||
|
{{ form.email }}
|
||||||
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label small fw-bold text-muted">Phone Type</label>
|
<label class="form-label small fw-bold text-muted">Phone Type</label>
|
||||||
{{ form.phone_type }}
|
{{ form.phone_type }}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ from django.contrib import messages
|
|||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.conf import settings
|
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 .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 logging
|
||||||
import zoneinfo
|
import zoneinfo
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -157,7 +157,7 @@ def voter_list(request):
|
|||||||
|
|
||||||
if query:
|
if query:
|
||||||
query = query.strip()
|
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:
|
if "," in query:
|
||||||
parts = [p.strip() for p in query.split(",")]
|
parts = [p.strip() for p in query.split(",")]
|
||||||
@ -358,7 +358,6 @@ def edit_likelihood(request, likelihood_id):
|
|||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = VoterLikelihoodForm(request.POST, instance=likelihood, tenant=tenant)
|
form = VoterLikelihoodForm(request.POST, instance=likelihood, tenant=tenant)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
# Check for conflict with another record of same election_type
|
|
||||||
election_type = form.cleaned_data['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():
|
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()
|
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'):
|
if data.get('address'):
|
||||||
voters = voters.filter(Q(address__icontains=data['address']) | Q(address_street__icontains=data['address']))
|
voters = voters.filter(Q(address__icontains=data['address']) | Q(address_street__icontains=data['address']))
|
||||||
if data.get('voter_id'):
|
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'):
|
if data.get('birth_month'):
|
||||||
voters = voters.filter(birthdate__month=data['birth_month'])
|
voters = voters.filter(birthdate__month=data['birth_month'])
|
||||||
if data.get('city'):
|
if data.get('city'):
|
||||||
@ -498,6 +497,8 @@ def voter_advanced_search(request):
|
|||||||
voters = voters.filter(district=data['district'])
|
voters = voters.filter(district=data['district'])
|
||||||
if data.get('precinct'):
|
if data.get('precinct'):
|
||||||
voters = voters.filter(precinct=data['precinct'])
|
voters = voters.filter(precinct=data['precinct'])
|
||||||
|
if data.get('email'):
|
||||||
|
voters = voters.filter(email__icontains=data['email'])
|
||||||
if data.get('phone_type'):
|
if data.get('phone_type'):
|
||||||
voters = voters.filter(phone_type=data['phone_type'])
|
voters = voters.filter(phone_type=data['phone_type'])
|
||||||
if data.get('is_targeted'):
|
if data.get('is_targeted'):
|
||||||
@ -563,7 +564,7 @@ def export_voters_csv(request):
|
|||||||
if data.get('address'):
|
if data.get('address'):
|
||||||
voters = voters.filter(Q(address__icontains=data['address']) | Q(address_street__icontains=data['address']))
|
voters = voters.filter(Q(address__icontains=data['address']) | Q(address_street__icontains=data['address']))
|
||||||
if data.get('voter_id'):
|
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'):
|
if data.get('birth_month'):
|
||||||
voters = voters.filter(birthdate__month=data['birth_month'])
|
voters = voters.filter(birthdate__month=data['birth_month'])
|
||||||
if data.get('city'):
|
if data.get('city'):
|
||||||
@ -574,6 +575,8 @@ def export_voters_csv(request):
|
|||||||
voters = voters.filter(district=data['district'])
|
voters = voters.filter(district=data['district'])
|
||||||
if data.get('precinct'):
|
if data.get('precinct'):
|
||||||
voters = voters.filter(precinct=data['precinct'])
|
voters = voters.filter(precinct=data['precinct'])
|
||||||
|
if data.get('email'):
|
||||||
|
voters = voters.filter(email__icontains=data['email'])
|
||||||
if data.get('phone_type'):
|
if data.get('phone_type'):
|
||||||
voters = voters.filter(phone_type=data['phone_type'])
|
voters = voters.filter(phone_type=data['phone_type'])
|
||||||
if data.get('is_targeted'):
|
if data.get('is_targeted'):
|
||||||
@ -842,7 +845,7 @@ def voter_search_json(request):
|
|||||||
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
||||||
voters = Voter.objects.filter(tenant=tenant)
|
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:
|
if "," in query:
|
||||||
parts = [p.strip() for p in query.split(",") ]
|
parts = [p.strip() for p in query.split(",") ]
|
||||||
@ -925,6 +928,7 @@ def volunteer_add(request):
|
|||||||
'form': form,
|
'form': form,
|
||||||
'tenant': tenant,
|
'tenant': tenant,
|
||||||
'selected_tenant': tenant,
|
'selected_tenant': tenant,
|
||||||
|
'is_create': True,
|
||||||
}
|
}
|
||||||
return render(request, 'core/volunteer_detail.html', context)
|
return render(request, 'core/volunteer_detail.html', context)
|
||||||
|
|
||||||
@ -1993,21 +1997,5 @@ def profile(request):
|
|||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
u_form = UserUpdateForm(request.POST, instance=request.user)
|
u_form = UserUpdateForm(request.POST, instance=request.user)
|
||||||
v_form = VolunteerProfileForm(request.POST, instance=volunteer) if volunteer else None
|
# v_form = VolunteerProfileForm(request.POST, instance=volunteer) if volunteer else None # Removed VolunteerProfileForm
|
||||||
|
v_form = None # Set v_form to None after removal
|
||||||
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)
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user