From 78457e14288d63b6ae9cfc7ce9bbf691cb0b42d5 Mon Sep 17 00:00:00 2001 From: Laurent Date: Sun, 13 Jul 2025 21:20:22 +0200 Subject: [PATCH] fixes and improvements --- bizdev/admin.py | 63 ++++++++++--------- bizdev/filters.py | 47 +++++++++++++- ...eclination_reason_alter_activity_status.py | 23 +++++++ bizdev/models.py | 44 ++++++++----- bizdev/templates/admin/bizdev/app_index.html | 25 -------- .../admin/bizdev/prospect/change_list.html | 14 +---- padelclub_backend/urls.py | 2 +- 7 files changed, 129 insertions(+), 89 deletions(-) create mode 100644 bizdev/migrations/0002_activity_declination_reason_alter_activity_status.py delete mode 100644 bizdev/templates/admin/bizdev/app_index.html diff --git a/bizdev/admin.py b/bizdev/admin.py index 916b63f..147ced8 100644 --- a/bizdev/admin.py +++ b/bizdev/admin.py @@ -12,9 +12,9 @@ import io import time import logging -from .models import Entity, Prospect, Activity, Status, ActivityType, EmailTemplate +from .models import Entity, Prospect, Activity, Status, ActivityType, EmailTemplate, DeclinationReason from .forms import FileImportForm, EmailTemplateSelectionForm -from .filters import StaffUserFilter, ProspectProfileFilter +from .filters import ProspectStatusFilter, StaffUserFilter, ProspectProfileFilter, ProspectDeclineReasonFilter from tournaments.models import CustomUser from tournaments.models.enums import UserOrigin @@ -92,14 +92,14 @@ create_activity_for_prospect.short_description = "Create event" @admin.register(Prospect) class ProspectAdmin(SyncedObjectAdmin): - readonly_fields = ['related_events', 'entity_names', 'current_status'] + readonly_fields = ['related_activities', 'entity_names', 'current_status', 'id'] fieldsets = [ (None, { - 'fields': ['related_events', 'entity_names', 'first_name', 'last_name', 'email', 'phone', 'name_unsure', 'official_user', 'entities'] + 'fields': ['related_activities', 'id', 'entity_names', 'related_user', 'first_name', 'last_name', 'email', 'phone', 'official_user', 'name_unsure', 'entities'] }), ] list_display = ('entity_names', 'first_name', 'last_name', 'email', 'last_update', 'current_status') - list_filter = ('creation_date', StaffUserFilter, 'source', ProspectProfileFilter) + list_filter = (ProspectStatusFilter, ProspectDeclineReasonFilter, 'creation_date', StaffUserFilter, 'source', ProspectProfileFilter) search_fields = ('first_name', 'last_name', 'email') date_hierarchy = 'creation_date' change_list_template = "admin/bizdev/prospect/change_list.html" @@ -107,27 +107,16 @@ class ProspectAdmin(SyncedObjectAdmin): filter_horizontal = ['entities'] actions = ['send_email', create_activity_for_prospect, contacted_by_sms, mark_as_should_test, mark_as_testing, mark_as_customer, declined_too_expensive, declined_use_something_else] - def related_events(self, obj): - events = obj.events.all() - if events: - event_links = [] - for event in events: - url = f"/kingdom/bizdev/event/{event.id}/change/" - event_links.append(f'{event.html_desc()}') - return format_html('
'.join(event_links)) + def related_activities(self, obj): + activities = obj.activities.all() + if activities: + activity_links = [] + for activity in activities: + url = f"/kingdom/bizdev/activity/{activity.id}/change/" + activity_links.append(f'{activity.html_desc()}') + return format_html('
'.join(activity_links)) return "No events" - - related_events.short_description = "Related Events" - - def changelist_view(self, request, extra_context=None): - # Add the URL with a filter for the current user - user_filter_url = "{}?related_user__id__exact={}".format( - reverse('admin:bizdev_prospect_changelist'), - request.user.id - ) - extra_context = extra_context or {} - extra_context['user_filter_url'] = user_filter_url - return super().changelist_view(request, extra_context=extra_context) + related_activities.short_description = "Related Activities" def get_urls(self): urls = super().get_urls() @@ -292,7 +281,8 @@ class ProspectAdmin(SyncedObjectAdmin): # Create Event if attachment_text or status is provided if attachment_text or status_text: # Map status text to Status enum - status_value = Status.NONE + status_value = None + declination_reason = None if status_text: if 'CONTACTED' in status_text: status_value = Status.CONTACTED @@ -302,19 +292,30 @@ class ProspectAdmin(SyncedObjectAdmin): status_value = Status.SHOULD_TEST elif 'CUSTOMER' in status_text: status_value = Status.CUSTOMER + elif 'TESTING' in status_text: + status_value = Status.TESTING elif 'LOST' in status_text: status_value = Status.LOST elif 'DECLINED_TOO_EXPENSIVE' in status_text: - status_value = Status.DECLINED_TOO_EXPENSIVE - elif 'DECLINED_USE_SOMETHING_ELSE' in status_text: - status_value = Status.DECLINED_USE_SOMETHING_ELSE - elif 'DECLINED_UNRELATED' in status_text or 'NOK' in status_text: + status_value = Status.DECLINED + declination_reason = DeclinationReason.TOO_EXPENSIVE + elif 'USE_OTHER_PRODUCT' in status_text: + status_value = Status.DECLINED + declination_reason = DeclinationReason.USE_OTHER_PRODUCT + elif 'USE_ANDROID' in status_text: + status_value = Status.DECLINED + declination_reason = DeclinationReason.USE_ANDROID + elif 'NOK' in status_text: + status_value = Status.DECLINED + declination_reason = DeclinationReason.UNKNOWN + elif 'DECLINED_UNRELATED' in status_text: status_value = Status.DECLINED_UNRELATED activity = Activity.objects.create( type=ActivityType.SMS, attachment_text=attachment_text, status=status_value, + declination_reason=declination_reason, description=f"Imported from CSV - Status: {status_text}" if status_text else "Imported from CSV" ) activity.prospects.add(prospect) @@ -378,7 +379,7 @@ class ProspectAdmin(SyncedObjectAdmin): @admin.register(Activity) class ActivityAdmin(SyncedObjectAdmin): - list_display = ('creation_date', 'type', 'description', 'attachment_text') + list_display = ('creation_date', 'status', 'type', 'description', 'attachment_text') list_filter = ('status', 'type') search_fields = ('description',) filter_horizontal = ('prospects',) diff --git a/bizdev/filters.py b/bizdev/filters.py index 782481f..8edb770 100644 --- a/bizdev/filters.py +++ b/bizdev/filters.py @@ -1,12 +1,12 @@ import django_filters -from django.db.models import Q +from django.db.models import Max, F, Q from django.contrib.auth import get_user_model from django.contrib import admin from django.utils import timezone from dateutil.relativedelta import relativedelta -from .models import Activity, Prospect +from .models import Activity, Prospect, Status, DeclinationReason User = get_user_model() @@ -68,3 +68,46 @@ class ProspectProfileFilter(admin.SimpleListFilter): official_user__isnull=False, official_user__events__isnull=True ) + +class ProspectStatusFilter(admin.SimpleListFilter): + title = 'Status' + parameter_name = 'status' + + def lookups(self, request, model_admin): + return [(tag.name, tag.value) for tag in Status] + + def queryset(self, request, queryset): + if self.value(): + # Get prospects whose most recent activity has the selected status + return queryset.filter( + activities__status=self.value() + ).annotate( + latest_activity_date=Max('activities__creation_date') + ).filter( + activities__creation_date=F('latest_activity_date'), + activities__status=self.value() + ).distinct() + else: + return queryset + +class ProspectDeclineReasonFilter(admin.SimpleListFilter): + title = 'Decline reason' + parameter_name = 'reason' + + def lookups(self, request, model_admin): + return [(tag.name, tag.value) for tag in DeclinationReason] + + + def queryset(self, request, queryset): + if self.value(): + # Get prospects whose most recent activity has the selected status + return queryset.filter( + activities__declination_reason=self.value() + ).annotate( + latest_activity_date=Max('activities__creation_date') + ).filter( + activities__creation_date=F('latest_activity_date'), + activities__declination_reason=self.value() + ).distinct() + else: + return queryset diff --git a/bizdev/migrations/0002_activity_declination_reason_alter_activity_status.py b/bizdev/migrations/0002_activity_declination_reason_alter_activity_status.py new file mode 100644 index 0000000..9508faf --- /dev/null +++ b/bizdev/migrations/0002_activity_declination_reason_alter_activity_status.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1 on 2025-07-10 13:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bizdev', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='activity', + name='declination_reason', + field=models.CharField(blank=True, choices=[('TOO_EXPENSIVE', 'Too expensive'), ('USE_OTHER_PRODUCT', 'Use other product'), ('USE_ANDROID', 'Use Android'), ('UNKNOWN', 'Unknown')], max_length=50, null=True), + ), + migrations.AlterField( + model_name='activity', + name='status', + field=models.CharField(blank=True, choices=[('CONTACTED', 'Contacted'), ('RESPONDED', 'Responded'), ('SHOULD_TEST', 'Should test'), ('TESTING', 'Testing'), ('CUSTOMER', 'Customer'), ('LOST', 'Lost customer'), ('DECLINED', 'Declined'), ('DECLINED_UNRELATED', 'Declined without significance')], max_length=50, null=True), + ), + ] diff --git a/bizdev/models.py b/bizdev/models.py index 86b5031..785910b 100644 --- a/bizdev/models.py +++ b/bizdev/models.py @@ -11,18 +11,21 @@ from sync.models import BaseModel User = get_user_model() class Status(models.TextChoices): - NONE = 'NONE', 'None' CONTACTED = 'CONTACTED', 'Contacted' RESPONDED = 'RESPONDED', 'Responded' SHOULD_TEST = 'SHOULD_TEST', 'Should test' TESTING = 'TESTING', 'Testing' CUSTOMER = 'CUSTOMER', 'Customer' LOST = 'LOST', 'Lost customer' - DECLINED_TOO_EXPENSIVE = 'DECLINED_TOO_EXPENSIVE', 'Too expensive' - DECLINED_USE_SOMETHING_ELSE = 'DECLINED_USE_SOMETHING_ELSE', 'Use something else' - DECLINED_OTHER = 'DECLINED_OTHER', 'Declined other reason' + DECLINED = 'DECLINED', 'Declined' DECLINED_UNRELATED = 'DECLINED_UNRELATED', 'Declined without significance' +class DeclinationReason(models.TextChoices): + TOO_EXPENSIVE = 'TOO_EXPENSIVE', 'Too expensive' + USE_OTHER_PRODUCT = 'USE_OTHER_PRODUCT', 'Use other product' + USE_ANDROID = 'USE_ANDROID', 'Use Android' + UNKNOWN = 'UNKNOWN', 'Unknown' + class ActivityType(models.TextChoices): MAIL = 'MAIL', 'Mailing List' SMS = 'SMS', 'SMS Campaign' @@ -60,11 +63,11 @@ class Prospect(BaseModel): def delete_dependencies(self): pass - class Meta: - permissions = [ - ("manage_prospects", "Can manage prospects"), - ("view_prospects", "Can view prospects"), - ] + # class Meta: + # permissions = [ + # ("manage_prospects", "Can manage prospects"), + # ("view_prospects", "Can view prospects"), + # ] def current_status(self): last_activity = self.activities.exclude(status=None).order_by('-creation_date').first() @@ -72,6 +75,12 @@ class Prospect(BaseModel): return last_activity.status return None + def current_declination_reason(self): + last_activity = self.activities.exclude(status=None).order_by('-creation_date').first() + if last_activity: + return last_activity.declination_reason + return None + def entity_names(self): entity_names = [entity.name for entity in self.entities.all()] return " - ".join(entity_names) @@ -83,7 +92,8 @@ class Prospect(BaseModel): return self.full_name() class Activity(BaseModel): - status = models.CharField(max_length=50, default=Status.NONE, choices=Status.choices, null=True, blank=True) + status = models.CharField(max_length=50, choices=Status.choices, null=True, blank=True) + declination_reason = models.CharField(max_length=50, choices=DeclinationReason.choices, null=True, blank=True) type = models.CharField(max_length=20, choices=ActivityType.choices, null=True, blank=True) description = models.TextField(null=True, blank=True) attachment_text = models.TextField(null=True, blank=True) @@ -103,16 +113,16 @@ class Activity(BaseModel): class Meta: verbose_name_plural = "Activities" ordering = ['-creation_date'] - permissions = [ - ("manage_events", "Can manage events"), - ("view_events", "Can view events"), - ] + # permissions = [ + # ("manage_events", "Can manage events"), + # ("view_events", "Can view events"), + # ] - def __str__(self): - return f"{self.get_type_display()} - {self.creation_date.date()}" + # def __str__(self): + # return f"{self.get_type_display()} - {self.creation_date.date()}" def html_desc(self): - fields = [field for field in [self.creation_date.strftime("%d/%m/%Y %H:%M"), self.type, self.status, self.attachment_text, self.description] if field is not None] + fields = [field for field in [self.creation_date.strftime("%d/%m/%Y %H:%M"), self.status, self.attachment_text, self.description, self.type] if field is not None] html = '' for field in fields: html += f'' diff --git a/bizdev/templates/admin/bizdev/app_index.html b/bizdev/templates/admin/bizdev/app_index.html deleted file mode 100644 index 9c83f1a..0000000 --- a/bizdev/templates/admin/bizdev/app_index.html +++ /dev/null @@ -1,25 +0,0 @@ - -{% extends "admin/app_index.html" %} -{% load i18n %} - -{% block content %} -
- - -
- -{{ block.super }} -{% endblock %} diff --git a/bizdev/templates/admin/bizdev/prospect/change_list.html b/bizdev/templates/admin/bizdev/prospect/change_list.html index ecf6373..a29e9f9 100644 --- a/bizdev/templates/admin/bizdev/prospect/change_list.html +++ b/bizdev/templates/admin/bizdev/prospect/change_list.html @@ -5,18 +5,6 @@
  • Import Import App Users - Reset +
  • {% endblock %} - -{% block search %} - - {{ block.super }} - -
    - - My Prospects - -
    - -{% endblock %} diff --git a/padelclub_backend/urls.py b/padelclub_backend/urls.py index e4f5c5c..9e7a4b4 100644 --- a/padelclub_backend/urls.py +++ b/padelclub_backend/urls.py @@ -34,7 +34,7 @@ urlpatterns = [ path('kingdom/debug/player-license-lookup/', get_player_license_info, name='player_license_lookup'), path('kingdom/debug/bulk-license-lookup/', bulk_license_lookup, name='bulk_license_lookup'), path('kingdom/debug/explore-api-endpoints/', explore_fft_api_endpoints, name='explore_api_endpoints'), - path('kingdom/bizdev/', include('bizdev.admin_urls')), + # path('kingdom/bizdev/', include('bizdev.admin_urls')), path('kingdom/', admin.site.urls), path('api-auth/', include('rest_framework.urls')), path('dj-auth/', include('django.contrib.auth.urls')),
    {field}