From 1dfb7ebbf1cccd9b8d66ff0ae6d910b5f5b083ce Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Mon, 26 Jan 2026 14:28:46 +0000 Subject: [PATCH] Autosave: 20260126-142846 --- core/__pycache__/admin.cpython-311.pyc | Bin 74954 -> 83837 bytes core/__pycache__/forms.cpython-311.pyc | Bin 15471 -> 16169 bytes core/__pycache__/models.cpython-311.pyc | Bin 25879 -> 26091 bytes core/__pycache__/views.cpython-311.pyc | Bin 26607 -> 27736 bytes core/admin.py | 171 ++++++++- core/forms.py | 11 +- ...olunteer_first_name_volunteer_last_name.py | 23 ++ core/migrations/0020_remove_volunteer_name.py | 17 + ...t_name_volunteer_last_name.cpython-311.pyc | Bin 0 -> 1002 bytes ...0020_remove_volunteer_name.cpython-311.pyc | Bin 0 -> 718 bytes core/models.py | 9 +- .../admin/volunteer_change_list.html | 38 ++ core/templates/core/index.html | 339 +++++++++++------- core/views.py | 15 +- 14 files changed, 476 insertions(+), 147 deletions(-) create mode 100644 core/migrations/0019_volunteer_first_name_volunteer_last_name.py create mode 100644 core/migrations/0020_remove_volunteer_name.py create mode 100644 core/migrations/__pycache__/0019_volunteer_first_name_volunteer_last_name.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0020_remove_volunteer_name.cpython-311.pyc create mode 100644 core/templates/admin/volunteer_change_list.html diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 5ea9b26d96fb11ef1894249c2b22e12fb29b8bcd..bcabe2c7ac1ec249da8fa56db60777c6adb2e64b 100644 GIT binary patch delta 13404 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 delta 9970 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 diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index 73a5120cb5df46ade2b16ffb168642b65b893af5..d062e201e686e2e2b962305ce4d37c400ce55e02 100644 GIT binary patch delta 205 zcmaD~v9gYDIWI340}woXQkE&GFp*D!(PE>zAQKl8gF8ctObbJb?BolK((JM+a={Fm z@|*3LG|eVAX{_fG4$IFe%_~VwE!t${$|xrT=X&NA( zdCqyC^FHUk=bWDXhTQpuE))rnYpoM_)bK#s-Y0x^y6rPwzy+EFVSke#V{jEt`fCLd zsR;A_ADu{tsc55ZT4Q(Pa6WKcujuABfv>&7ATX9}x1H|a3}x-oF`4I7-I#HGp;Cl` zS)SuA(_UyX3)5^_AFEpxokHd|&+4d(_BECD36)I|mEt+$6RMI^+z3%jCyHXFiunX* zjM-0dM@g(K!nq{)y(3UuLDOmPA?(ZU>D*389UKVui0Fg;Za;h%-l3n-o#Ww^8Y@&j zWv)*NZ*pTB8;Nk*A5`elYlXs7o;Rd3+GtAq8?MI7U@5po*hvePcEiJFNBK{Z=LgBN zD0y>|cR>nX*MBW86a}?yn8N`dwmP8EZ-)AiOWw$*{Y8T`TRc?Lfu+w&$#i=3r3qC@ zsIRFLnY8vB3hEIaBXDR(JJ+H)a9m0`gK`XPrSJe+%FE!U(F`l0O1GtIx+G_*x)}Lf zzhLn#T6{T+Zz}k+TsbY|-4N^Uwy{n1;r8lrd+d5Oz!%-sq7%mmyWn>Bao$;2f3oKeZzS+m%ntAN z?kjbns0){eDY$eRyXy_G)EgvTXz1Ht!!k012GkxyEfZVj<7NYGj~p zt*T`rkM$d}LfggQezytH!^&69m^o-LkWw> zY$tt$P#;;*M8p*R^$~sa`Vor4KBD@m3{A4fS9SK8N~pt}#Xf64*6e-GoqG)D9)any z$)p$9QcoQ0K62AET?I1Us(LHEW1RLrKDF5)K7Z@>XQhU@r(eD1fuo8)?S_uUu zN~vY(E7MKfV2qC{coD#s+Vhlt#`)Jmy^|rIMM7H@(jw73W)>P>C07NAP)@ zWrax;RNa0vTB_|?Y1}rl5_~BsiY}HqEegUl5nr`ALh4FIr$9_Utz#71iPfIL(=30^ zs#P&4#+d1!sLU~8FDT~Gly;kvHY&8+$An>McQgxfIwsZCrcbbZEtkMUa5COjw;CWH zhw1~EZuek^&yrq5eH4*<6$9Cc6Wd(q_L_0VCmRZg|3dH;_5QW2(-MD9;SVByG8kmJ z!dA^iXB*Wa$Yy)+Go!xa)@&z^``u@DTJeK_7}sQFVy+{ZZG&h7oJn!um4+#Bp?AkC zD8u}~j!a&qk?IvvJw9X^j|cMfE;7Y5cNf zj5XFn6@F`MTE~m=+A_r+rC1)Mk(EJQ31Gj`u20N_5gTy4>7-bc(5h&D!L-aR0}c~& z{Z^QjBE9`XeNoNKIJ{6}!&r+2s_|}1sPOM|<4&!MWO7;9tkkPU_w_5<5-}AC%~Iyj z2s{{WJq4xl`BooDPGSwXB^a@lGxLvVQuOeEx-9u5XoUd24u;COBp)IPC5G5cDphn$ zykxTYgSV2kh&`bkeI=O$wKy5NCsy)~;|F$40wm#zj`hja%-4wHnK=}{n)3PO zRFk!YKf=X&A9)BOSl%@bKD^)cQsy5hzf0aHe;541_yl<){2%!6K_pwhi)FK)F!(XP d5IHXvr-Ad6@IRcE&KehqUs`tN#qTQX;TQau0?7aX diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index e1e7dd1a08ba8257de33dfeb673c9c0e2acf34ac..51419e30f9998ecae34ddfb6aa6b08c92e770d84 100644 GIT binary patch delta 5946 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 diff --git a/core/admin.py b/core/admin.py index 02ac347..40b43cc 100644 --- a/core/admin.py +++ b/core/admin.py @@ -16,7 +16,8 @@ from .models import ( ) from .forms import ( VoterImportForm, EventImportForm, EventParticipationImportForm, - DonationImportForm, InteractionImportForm, VoterLikelihoodImportForm + DonationImportForm, InteractionImportForm, VoterLikelihoodImportForm, + VolunteerImportForm ) logger = logging.getLogger(__name__) @@ -78,6 +79,14 @@ INTERACTION_MAPPABLE_FIELDS = [ ('notes', 'Notes'), ] + +VOLUNTEER_MAPPABLE_FIELDS = [ + ('first_name', 'First Name'), + ('last_name', 'Last Name'), + ('email', 'Email'), + ('phone', 'Phone'), +] + VOTER_LIKELIHOOD_MAPPABLE_FIELDS = [ ('voter_id', 'Voter ID'), ('election_type', 'Election Type (Name)'), @@ -565,12 +574,162 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): return render(request, "admin/import_csv.html", context) @admin.register(Volunteer) -class VolunteerAdmin(admin.ModelAdmin): - list_display = ('name', 'email', 'phone', 'tenant', 'user') +class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin): + list_display = ('first_name', 'last_name', 'email', 'phone', 'tenant', 'user') list_filter = ('tenant',) - search_fields = ('name', 'email', 'phone') + search_fields = ('first_name', 'last_name', 'email', 'phone') inlines = [VolunteerEventInline, InteractionInline] filter_horizontal = ('interests',) + change_list_template = "admin/volunteer_change_list.html" + + def changelist_view(self, request, extra_context=None): + extra_context = extra_context or {} + from core.models import Tenant + extra_context["tenants"] = Tenant.objects.all() + return super().changelist_view(request, extra_context=extra_context) + + def get_urls(self): + urls = super().get_urls() + my_urls = [ + path('download-errors/', self.admin_site.admin_view(self.download_errors), name='volunteer-download-errors'), + path('import-volunteers/', self.admin_site.admin_view(self.import_volunteers), name='import-volunteers'), + ] + return my_urls + urls + + def import_volunteers(self, request): + if request.method == "POST": + if "_preview" in request.POST: + file_path = request.POST.get('file_path') + tenant_id = request.POST.get('tenant') + tenant = Tenant.objects.get(id=tenant_id) + mapping = {} + for field_name, _ in VOLUNTEER_MAPPABLE_FIELDS: + mapping[field_name] = request.POST.get(f'map_{field_name}') + try: + with open(file_path, 'r', encoding='UTF-8') as f: + reader = csv.DictReader(f) + total_count = 0 + create_count = 0 + update_count = 0 + preview_data = [] + for row in reader: + total_count += 1 + email = row.get(mapping.get('email')) + exists = Volunteer.objects.filter(tenant=tenant, email=email).exists() + if exists: + update_count += 1 + action = 'update' + else: + create_count += 1 + action = 'create' + if len(preview_data) < 10: + preview_data.append({ + 'action': action, + 'identifier': email, + 'details': f"{row.get(mapping.get('first_name', '')) or ''} {row.get(mapping.get('last_name', '')) or ''}".strip() + }) + context = self.admin_site.each_context(request) + context.update({ + 'title': "Import Preview", + 'total_count': total_count, + 'create_count': create_count, + 'update_count': update_count, + 'preview_data': preview_data, + 'mapping': mapping, + 'file_path': file_path, + 'tenant_id': tenant_id, + 'action_url': request.path, + 'opts': self.model._meta, + }) + return render(request, "admin/import_preview.html", context) + except Exception as e: + self.message_user(request, f"Error processing preview: {e}", level=messages.ERROR) + return redirect("..") + + elif "_import" in request.POST: + file_path = request.POST.get('file_path') + tenant_id = request.POST.get('tenant') + tenant = Tenant.objects.get(id=tenant_id) + mapping = {} + for field_name, _ in VOLUNTEER_MAPPABLE_FIELDS: + mapping[field_name] = request.POST.get(f'map_{field_name}') + try: + with open(file_path, 'r', encoding='UTF-8') as f: + reader = csv.DictReader(f) + count = 0 + errors = 0 + failed_rows = [] + for row in reader: + try: + email = row.get(mapping.get('email')) + if not email: + row["Import Error"] = "Missing email" + failed_rows.append(row) + errors += 1 + continue + volunteer_data = {} + for field_name, csv_col in mapping.items(): + if csv_col: + val = row.get(csv_col) + if val is not None and str(val).strip() != '': + if field_name == 'email': continue + volunteer_data[field_name] = val + Volunteer.objects.update_or_create( + tenant=tenant, + email=email, + defaults=volunteer_data + ) + count += 1 + except Exception as e: + logger.error(f"Error importing volunteer: {e}") + row["Import Error"] = str(e) + failed_rows.append(row) + errors += 1 + if os.path.exists(file_path): + os.remove(file_path) + self.message_user(request, f"Successfully imported {count} volunteers.") + request.session[f"{self.model._meta.model_name}_import_errors"] = failed_rows + request.session.modified = True + if errors > 0: + error_url = reverse("admin:volunteer-download-errors") + self.message_user(request, mark_safe(f"Failed to import {errors} rows. Download failed records"), level=messages.WARNING) + return redirect("..") + except Exception as e: + self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) + 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("..") + with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: + for chunk in csv_file.chunks(): + tmp.write(chunk) + file_path = tmp.name + with open(file_path, 'r', encoding='UTF-8') as f: + reader = csv.reader(f) + headers = next(reader) + context = self.admin_site.each_context(request) + context.update({ + 'title': "Map Volunteer Fields", + 'headers': headers, + 'model_fields': VOLUNTEER_MAPPABLE_FIELDS, + 'tenant_id': tenant.id, + 'file_path': file_path, + 'action_url': request.path, + 'opts': self.model._meta, + }) + return render(request, "admin/import_mapping.html", context) + else: + form = VolunteerImportForm() + context = self.admin_site.each_context(request) + context['form'] = form + context['title'] = "Import Volunteers" + context['opts'] = self.model._meta + return render(request, "admin/import_csv.html", context) @admin.register(VolunteerEvent) class VolunteerEventAdmin(admin.ModelAdmin): @@ -982,7 +1141,7 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): list_display = ('id', 'voter', 'volunteer', 'type', 'date', 'description') list_filter = ('voter__tenant', 'type', 'date', 'volunteer') - search_fields = ('voter__first_name', 'voter__last_name', 'voter__voter_id', 'description', 'volunteer__name') + search_fields = ('voter__first_name', 'voter__last_name', 'voter__voter_id', 'description', 'volunteer__first_name', 'volunteer__last_name') change_list_template = "admin/interaction_change_list.html" def get_urls(self): @@ -1376,4 +1535,4 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin): @admin.register(CampaignSettings) class CampaignSettingsAdmin(admin.ModelAdmin): list_display = ('tenant', 'donation_goal') - list_filter = ('tenant',) + list_filter = ('tenant',) \ No newline at end of file diff --git a/core/forms.py b/core/forms.py index 9afddb6..d9ca81b 100644 --- a/core/forms.py +++ b/core/forms.py @@ -164,4 +164,13 @@ class VoterLikelihoodImportForm(forms.Form): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['tenant'].widget.attrs.update({'class': 'form-control form-select'}) - self.fields['file'].widget.attrs.update({'class': 'form-control'}) \ No newline at end of file + self.fields['file'].widget.attrs.update({'class': 'form-control'}) + +class VolunteerImportForm(forms.Form): + tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), label="Campaign") + file = forms.FileField(label="Select CSV file") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['tenant'].widget.attrs.update({'class': 'form-control form-select'}) + self.fields['file'].widget.attrs.update({'class': 'form-control'}) diff --git a/core/migrations/0019_volunteer_first_name_volunteer_last_name.py b/core/migrations/0019_volunteer_first_name_volunteer_last_name.py new file mode 100644 index 0000000..313fb1e --- /dev/null +++ b/core/migrations/0019_volunteer_first_name_volunteer_last_name.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2026-01-26 05:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0018_event_end_time_event_name_event_start_time'), + ] + + operations = [ + migrations.AddField( + model_name='volunteer', + name='first_name', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AddField( + model_name='volunteer', + name='last_name', + field=models.CharField(blank=True, max_length=100), + ), + ] diff --git a/core/migrations/0020_remove_volunteer_name.py b/core/migrations/0020_remove_volunteer_name.py new file mode 100644 index 0000000..b38b88b --- /dev/null +++ b/core/migrations/0020_remove_volunteer_name.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.7 on 2026-01-26 13:59 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0019_volunteer_first_name_volunteer_last_name'), + ] + + operations = [ + migrations.RemoveField( + model_name='volunteer', + name='name', + ), + ] diff --git a/core/migrations/__pycache__/0019_volunteer_first_name_volunteer_last_name.cpython-311.pyc b/core/migrations/__pycache__/0019_volunteer_first_name_volunteer_last_name.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..30e91569b83e7ba0e4e5c6d64a8eaee2c997c126 GIT binary patch literal 1002 zcmb7By>HV%6u+|_$2LTwRwV>&mw*si>au}UMIaDJ2)Yz8csZQhHFfHbaCSo3GB6;f zZe5uQsQm-@Gm4ZUx|OM$t8^+0?;JY?!T@)^=l8q!aqsuur&`TIu#XVNc?+cp97N5Uzw&RdF{T6WOJ8YM=`)SObH!XBW>w9!P7#PRDgKCA{->DQC0idpm78BLi`!^u!lYd<>cp$Gl{YeP z-?aQ|(k=I00`1&bpH8F6R{&x-$x9HKiZKRCxN(eb&wrV|fb|T`i#=;}<>sk0oc+a` GzxxwAY6M^a literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/0020_remove_volunteer_name.cpython-311.pyc b/core/migrations/__pycache__/0020_remove_volunteer_name.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be774b4d5e2dfa0c392c4a1493d59f9ea8b17e8a GIT binary patch literal 718 zcmZuvJ8#rL5T5mmvqK~jKTu{_1VWsFl&!zb{^l%Cx2UA4S?iUxtG3S0KbgWiQ5{pdt~f` z0KqcI*?=*y;28+(GYA{m$m$9XY=A?0=Q6N_^BVf@Z{zlv4)S!U;z=rtGHFwqkJCD{ z2W0F6!hi{&Vm4$0+X8e(-5>-h>>thn0UjJa*tb`q&j-;Sa?_xY~O zs$znu_}f&K6JEqQE?s7Ed#C+Hrglsc^$Bt*Fl$PbPZMtU&*>hYx8OS9oZ?l6oa^(P zf2iWDX-=NNF%|-gB*ju0R`kj%%;hdVO)(QXkYjA=_zp9}!P!>$PUaX^J2Y$+;zvxX ziB#d9RPW1ioM32H3jfj=M$z4ftETDFI%a^)@h4?yYm`Su`IyvnacU3zC#<%uOWyQF z9e{uJOK{Y?a=dZlcx|J0Z9g~xQm01Mw!7|4pPQrByPn&9Tl-=Z7el!zcAEX>fNLhB zNxMkaS?}STpGWF4x%7)c`I1y^Gsb8LcKs(@TYhS9g?TmntG)(D7jB-I!0CUY4dNd& C^tVC) literal 0 HcmV?d00001 diff --git a/core/models.py b/core/models.py index 5c0a55b..9198711 100644 --- a/core/models.py +++ b/core/models.py @@ -285,7 +285,8 @@ class Event(models.Model): class Volunteer(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='volunteers') user = models.OneToOneField(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='volunteer_profile') - name = models.CharField(max_length=255) + first_name = models.CharField(max_length=100, blank=True) + last_name = models.CharField(max_length=100, blank=True) email = models.EmailField() phone = models.CharField(max_length=20, blank=True) interests = models.ManyToManyField(Interest, blank=True, related_name='volunteers') @@ -297,11 +298,11 @@ class Volunteer(models.Model): super().save(*args, **kwargs) def __str__(self): - return self.name + return f"{self.first_name} {self.last_name}".strip() or self.email class VolunteerEvent(models.Model): - volunteer = models.ForeignKey(Volunteer, on_delete=models.CASCADE, related_name='event_assignments') - event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='volunteer_assignments') + volunteer = models.ForeignKey(Volunteer, on_delete=models.CASCADE, related_name="event_assignments") + event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="volunteer_assignments") role = models.CharField(max_length=100) def __str__(self): diff --git a/core/templates/admin/volunteer_change_list.html b/core/templates/admin/volunteer_change_list.html new file mode 100644 index 0000000..0fe92f4 --- /dev/null +++ b/core/templates/admin/volunteer_change_list.html @@ -0,0 +1,38 @@ +{% extends "admin/change_list.html" %} +{% load i18n admin_urls static admin_list %} + +{% block object-tools-items %} +
  • + Import Volunteers +
  • + {{ block.super }} +{% endblock %} + +{% block search %} + {{ block.super }} +
    + + +
    + + +{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index f689bb2..e9b1663 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -2,157 +2,228 @@ {% load static %} {% block content %} -
    +
    {% if not selected_tenant %} -
    -
    -

    Welcome to Grassroots

    -

    Select a campaign to begin managing your voter database and organizing your efforts.

    - -
    - {% for tenant in tenants %} -
    -
    -
    -

    {{ tenant.name }}

    -

    {{ tenant.description|truncatewords:20 }}

    - Select Campaign -
    +
    +
    +

    Welcome to Campaign Manager

    +

    Select a campaign to view the dashboard.

    +
    + {% for tenant in tenants %} +
    +
    +
    +
    {{ tenant.name }}
    + Manage Campaign
    - {% empty %} -
    -
    No campaigns found. Please create one in the admin.
    -
    - {% endfor %}
    + {% endfor %}
    +
    {% else %} -
    -
    -
    -

    Dashboard: {{ selected_tenant.name }}

    - Switch Campaign +
    +
    +

    {{ selected_tenant.name }} Dashboard

    +

    Overview of voter engagement and field operations.

    +
    +
    + + +
    +
    +
    +
    +
    Active Voters
    +

    {{ metrics.total_registered_voters }}

    + View All →
    - - -
    -
    -
    - - -
    +
    +
    +
    +
    +
    +
    Target Voters
    +

    {{ metrics.total_target_voters }}

    + View Targets → +
    +
    +
    +
    +
    +
    +
    Supporting
    +

    {{ metrics.total_supporting }}

    + View Supporters → +
    +
    +
    +
    +
    +
    +
    Target Households
    +

    {{ metrics.total_target_households }}

    + View Map → +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    Door Visits
    +

    {{ metrics.total_door_visits }}

    - - -
    - - - - - - - - +
    +
    +
    +
    + +
    +
    Signs
    +

    {{ metrics.total_signs }}

    - -
    -
    -
    -
    -
    Voter Management
    -

    Access the full registry to manage individual voter profiles.

    -
    - View Registry +
    +
    +
    +
    +
    + +
    +
    Window Stickers
    +

    {{ metrics.total_window_stickers }}

    +
    +
    +
    +
    +
    +
    +
    Donation Goal
    +
    +

    ${{ metrics.total_donations|floatformat:0 }}

    + of ${{ metrics.donation_goal|floatformat:0 }} +
    +
    +
    +
    +
    + {{ metrics.donation_percentage }}%
    +
    + + +
    +
    +
    +
    +
    Recent Interactions
    + View All +
    +
    +
    + + + + + + + + + + + {% for interaction in recent_interactions %} + + + + + + + {% empty %} + + + + {% endfor %} + +
    VoterTypeDateNotes
    +
    {{ interaction.voter }}
    +
    {{ interaction.volunteer|default:"Staff" }}
    +
    + + {{ interaction.type }} + + + {{ interaction.date|date:"M d, Y" }} + + {{ interaction.description|truncatechars:50 }} +
    No recent interactions found.
    +
    +
    +
    +
    + +
    +
    +
    +
    Upcoming Events
    +
    +
    +
    + {% for event in upcoming_events %} +
    +
    +
    {{ event.name|default:event.event_type }}
    + + {{ event.event_type }} + +
    +
    + {{ event.date|date:"M d, Y" }} +
    +
    + {% empty %} +
    + No upcoming events. +
    + {% endfor %} +
    +
    +
    + + +
    +
    +
    Field Operations
    +
    +
    +
    + Total Volunteers + {{ metrics.volunteers_count }} +
    +
    + Total Interactions + {{ metrics.interactions_count }} +
    +
    + Total Events + {{ metrics.events_count }} +
    +
    +
    +
    +
    {% endif %}
    - - {% endblock %} \ No newline at end of file diff --git a/core/views.py b/core/views.py index 7cf3401..bb4a0e2 100644 --- a/core/views.py +++ b/core/views.py @@ -5,9 +5,10 @@ from django.urls import reverse from django.shortcuts import render, redirect, get_object_or_404 from django.db.models import Q, Sum from django.contrib import messages -from .models import Voter, Tenant, Interaction, Donation, VoterLikelihood, EventParticipation, Event, EventType, InteractionType, DonationMethod, ElectionType, CampaignSettings +from .models import Voter, Tenant, Interaction, Donation, VoterLikelihood, EventParticipation, Event, EventType, InteractionType, DonationMethod, ElectionType, CampaignSettings, Volunteer from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm, VoterImportForm import logging +from django.utils import timezone logger = logging.getLogger(__name__) @@ -20,6 +21,8 @@ def index(request): selected_tenant_id = request.session.get('tenant_id') selected_tenant = None metrics = {} + recent_interactions = [] + upcoming_events = [] if selected_tenant_id: selected_tenant = Tenant.objects.filter(id=selected_tenant_id).first() @@ -46,12 +49,20 @@ def index(request): 'total_donations': float(total_donations), 'donation_goal': float(donation_goal), 'donation_percentage': donation_percentage, + 'volunteers_count': Volunteer.objects.filter(tenant=selected_tenant).count(), + 'interactions_count': Interaction.objects.filter(voter__tenant=selected_tenant).count(), + 'events_count': Event.objects.filter(tenant=selected_tenant).count(), } + + recent_interactions = Interaction.objects.filter(voter__tenant=selected_tenant).order_by('-date')[:5] + upcoming_events = Event.objects.filter(tenant=selected_tenant, date__gte=timezone.now().date()).order_by('date')[:5] context = { 'tenants': tenants, 'selected_tenant': selected_tenant, 'metrics': metrics, + 'recent_interactions': recent_interactions, + 'upcoming_events': upcoming_events, } return render(request, 'core/index.html', context) @@ -387,4 +398,4 @@ def voter_geocode(request, voter_id): 'error': f"Geocoding failed: {error_msg or 'No results found.'}" }) - return JsonResponse({'success': False, 'error': 'Invalid request method.'}) \ No newline at end of file + return JsonResponse({'success': False, 'error': 'Invalid request method.'})