fixes and improvements

apikeys
Laurent 4 months ago
parent e8768c4980
commit 78457e1428
  1. 63
      bizdev/admin.py
  2. 47
      bizdev/filters.py
  3. 23
      bizdev/migrations/0002_activity_declination_reason_alter_activity_status.py
  4. 44
      bizdev/models.py
  5. 25
      bizdev/templates/admin/bizdev/app_index.html
  6. 14
      bizdev/templates/admin/bizdev/prospect/change_list.html
  7. 2
      padelclub_backend/urls.py

@ -12,9 +12,9 @@ import io
import time import time
import logging 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 .forms import FileImportForm, EmailTemplateSelectionForm
from .filters import StaffUserFilter, ProspectProfileFilter from .filters import ProspectStatusFilter, StaffUserFilter, ProspectProfileFilter, ProspectDeclineReasonFilter
from tournaments.models import CustomUser from tournaments.models import CustomUser
from tournaments.models.enums import UserOrigin from tournaments.models.enums import UserOrigin
@ -92,14 +92,14 @@ create_activity_for_prospect.short_description = "Create event"
@admin.register(Prospect) @admin.register(Prospect)
class ProspectAdmin(SyncedObjectAdmin): class ProspectAdmin(SyncedObjectAdmin):
readonly_fields = ['related_events', 'entity_names', 'current_status'] readonly_fields = ['related_activities', 'entity_names', 'current_status', 'id']
fieldsets = [ fieldsets = [
(None, { (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_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') search_fields = ('first_name', 'last_name', 'email')
date_hierarchy = 'creation_date' date_hierarchy = 'creation_date'
change_list_template = "admin/bizdev/prospect/change_list.html" change_list_template = "admin/bizdev/prospect/change_list.html"
@ -107,27 +107,16 @@ class ProspectAdmin(SyncedObjectAdmin):
filter_horizontal = ['entities'] 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] 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): def related_activities(self, obj):
events = obj.events.all() activities = obj.activities.all()
if events: if activities:
event_links = [] activity_links = []
for event in events: for activity in activities:
url = f"/kingdom/bizdev/event/{event.id}/change/" url = f"/kingdom/bizdev/activity/{activity.id}/change/"
event_links.append(f'<a href="{url}">{event.html_desc()}</a>') activity_links.append(f'<a href="{url}">{activity.html_desc()}</a>')
return format_html('<br>'.join(event_links)) return format_html('<br>'.join(activity_links))
return "No events" return "No events"
related_activities.short_description = "Related Activities"
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)
def get_urls(self): def get_urls(self):
urls = super().get_urls() urls = super().get_urls()
@ -292,7 +281,8 @@ class ProspectAdmin(SyncedObjectAdmin):
# Create Event if attachment_text or status is provided # Create Event if attachment_text or status is provided
if attachment_text or status_text: if attachment_text or status_text:
# Map status text to Status enum # Map status text to Status enum
status_value = Status.NONE status_value = None
declination_reason = None
if status_text: if status_text:
if 'CONTACTED' in status_text: if 'CONTACTED' in status_text:
status_value = Status.CONTACTED status_value = Status.CONTACTED
@ -302,19 +292,30 @@ class ProspectAdmin(SyncedObjectAdmin):
status_value = Status.SHOULD_TEST status_value = Status.SHOULD_TEST
elif 'CUSTOMER' in status_text: elif 'CUSTOMER' in status_text:
status_value = Status.CUSTOMER status_value = Status.CUSTOMER
elif 'TESTING' in status_text:
status_value = Status.TESTING
elif 'LOST' in status_text: elif 'LOST' in status_text:
status_value = Status.LOST status_value = Status.LOST
elif 'DECLINED_TOO_EXPENSIVE' in status_text: elif 'DECLINED_TOO_EXPENSIVE' in status_text:
status_value = Status.DECLINED_TOO_EXPENSIVE status_value = Status.DECLINED
elif 'DECLINED_USE_SOMETHING_ELSE' in status_text: declination_reason = DeclinationReason.TOO_EXPENSIVE
status_value = Status.DECLINED_USE_SOMETHING_ELSE elif 'USE_OTHER_PRODUCT' in status_text:
elif 'DECLINED_UNRELATED' in status_text or 'NOK' 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 status_value = Status.DECLINED_UNRELATED
activity = Activity.objects.create( activity = Activity.objects.create(
type=ActivityType.SMS, type=ActivityType.SMS,
attachment_text=attachment_text, attachment_text=attachment_text,
status=status_value, status=status_value,
declination_reason=declination_reason,
description=f"Imported from CSV - Status: {status_text}" if status_text else "Imported from CSV" description=f"Imported from CSV - Status: {status_text}" if status_text else "Imported from CSV"
) )
activity.prospects.add(prospect) activity.prospects.add(prospect)
@ -378,7 +379,7 @@ class ProspectAdmin(SyncedObjectAdmin):
@admin.register(Activity) @admin.register(Activity)
class ActivityAdmin(SyncedObjectAdmin): class ActivityAdmin(SyncedObjectAdmin):
list_display = ('creation_date', 'type', 'description', 'attachment_text') list_display = ('creation_date', 'status', 'type', 'description', 'attachment_text')
list_filter = ('status', 'type') list_filter = ('status', 'type')
search_fields = ('description',) search_fields = ('description',)
filter_horizontal = ('prospects',) filter_horizontal = ('prospects',)

@ -1,12 +1,12 @@
import django_filters 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.auth import get_user_model
from django.contrib import admin from django.contrib import admin
from django.utils import timezone from django.utils import timezone
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from .models import Activity, Prospect from .models import Activity, Prospect, Status, DeclinationReason
User = get_user_model() User = get_user_model()
@ -68,3 +68,46 @@ class ProspectProfileFilter(admin.SimpleListFilter):
official_user__isnull=False, official_user__isnull=False,
official_user__events__isnull=True 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

@ -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),
),
]

@ -11,18 +11,21 @@ from sync.models import BaseModel
User = get_user_model() User = get_user_model()
class Status(models.TextChoices): class Status(models.TextChoices):
NONE = 'NONE', 'None'
CONTACTED = 'CONTACTED', 'Contacted' CONTACTED = 'CONTACTED', 'Contacted'
RESPONDED = 'RESPONDED', 'Responded' RESPONDED = 'RESPONDED', 'Responded'
SHOULD_TEST = 'SHOULD_TEST', 'Should test' SHOULD_TEST = 'SHOULD_TEST', 'Should test'
TESTING = 'TESTING', 'Testing' TESTING = 'TESTING', 'Testing'
CUSTOMER = 'CUSTOMER', 'Customer' CUSTOMER = 'CUSTOMER', 'Customer'
LOST = 'LOST', 'Lost customer' LOST = 'LOST', 'Lost customer'
DECLINED_TOO_EXPENSIVE = 'DECLINED_TOO_EXPENSIVE', 'Too expensive' DECLINED = 'DECLINED', 'Declined'
DECLINED_USE_SOMETHING_ELSE = 'DECLINED_USE_SOMETHING_ELSE', 'Use something else'
DECLINED_OTHER = 'DECLINED_OTHER', 'Declined other reason'
DECLINED_UNRELATED = 'DECLINED_UNRELATED', 'Declined without significance' 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): class ActivityType(models.TextChoices):
MAIL = 'MAIL', 'Mailing List' MAIL = 'MAIL', 'Mailing List'
SMS = 'SMS', 'SMS Campaign' SMS = 'SMS', 'SMS Campaign'
@ -60,11 +63,11 @@ class Prospect(BaseModel):
def delete_dependencies(self): def delete_dependencies(self):
pass pass
class Meta: # class Meta:
permissions = [ # permissions = [
("manage_prospects", "Can manage prospects"), # ("manage_prospects", "Can manage prospects"),
("view_prospects", "Can view prospects"), # ("view_prospects", "Can view prospects"),
] # ]
def current_status(self): def current_status(self):
last_activity = self.activities.exclude(status=None).order_by('-creation_date').first() last_activity = self.activities.exclude(status=None).order_by('-creation_date').first()
@ -72,6 +75,12 @@ class Prospect(BaseModel):
return last_activity.status return last_activity.status
return None 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): def entity_names(self):
entity_names = [entity.name for entity in self.entities.all()] entity_names = [entity.name for entity in self.entities.all()]
return " - ".join(entity_names) return " - ".join(entity_names)
@ -83,7 +92,8 @@ class Prospect(BaseModel):
return self.full_name() return self.full_name()
class Activity(BaseModel): 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) type = models.CharField(max_length=20, choices=ActivityType.choices, null=True, blank=True)
description = models.TextField(null=True, blank=True) description = models.TextField(null=True, blank=True)
attachment_text = models.TextField(null=True, blank=True) attachment_text = models.TextField(null=True, blank=True)
@ -103,16 +113,16 @@ class Activity(BaseModel):
class Meta: class Meta:
verbose_name_plural = "Activities" verbose_name_plural = "Activities"
ordering = ['-creation_date'] ordering = ['-creation_date']
permissions = [ # permissions = [
("manage_events", "Can manage events"), # ("manage_events", "Can manage events"),
("view_events", "Can view events"), # ("view_events", "Can view events"),
] # ]
def __str__(self): # def __str__(self):
return f"{self.get_type_display()} - {self.creation_date.date()}" # return f"{self.get_type_display()} - {self.creation_date.date()}"
def html_desc(self): 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 = '<table><tr>' html = '<table><tr>'
for field in fields: for field in fields:
html += f'<td style="padding:0px 5px;">{field}</td>' html += f'<td style="padding:0px 5px;">{field}</td>'

@ -1,25 +0,0 @@
<!-- templates/admin/bizdev/app_index.html -->
{% extends "admin/app_index.html" %}
{% load i18n %}
{% block content %}
<div class="module" style="margin-bottom: 20px;">
<!-- <h2>{% trans "Actions" %}</h2> -->
<div class="form-row">
<a href="{% url 'bizdev_email_count' %}" class="button default" style="margin-right: 5px;">
{% trans "Count Users no event" %}
</a>
<a href="{% url 'bizdev_email_users' %}" class="button default" style="margin-right: 5px;">
{% trans "Insta send email no event" %}
</a>
<a href="{% url 'bizdev_email_with_tournaments_count' %}" class="button default" style="margin-right: 5px;">
{% trans "Count Users" %}
</a>
<a href="{% url 'email_users_with_tournaments' %}" class="button default" style="margin-right: 5px;">
{% trans "Insta send email" %}
</a>
</div>
</div>
{{ block.super }}
{% endblock %}

@ -5,18 +5,6 @@
<li> <li>
<a href="{% url 'admin:import_file' %}" class="addlink" style="margin-right: 5px;">Import</a> <a href="{% url 'admin:import_file' %}" class="addlink" style="margin-right: 5px;">Import</a>
<a href="{% url 'admin:import_app_users' %}" class="addlink" style="margin-right: 5px;">Import App Users</a> <a href="{% url 'admin:import_app_users' %}" class="addlink" style="margin-right: 5px;">Import App Users</a>
<a href="{% url 'admin:cleanup' %}" class="addlink" style="margin-right: 5px;">Reset</a> <!-- <a href="{% url 'admin:cleanup' %}" class="deletelink" style="margin-right: 5px;">Reset</a> -->
</li> </li>
{% endblock %} {% endblock %}
{% block search %}
{{ block.super }}
<div style="margin-top: 10px; margin-bottom: 10px;">
<a href="{{ user_filter_url }}" class="button">
My Prospects
</a>
</div>
{% endblock %}

@ -34,7 +34,7 @@ urlpatterns = [
path('kingdom/debug/player-license-lookup/', get_player_license_info, name='player_license_lookup'), 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/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/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('kingdom/', admin.site.urls),
path('api-auth/', include('rest_framework.urls')), path('api-auth/', include('rest_framework.urls')),
path('dj-auth/', include('django.contrib.auth.urls')), path('dj-auth/', include('django.contrib.auth.urls')),

Loading…
Cancel
Save