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 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'<a href="{url}">{event.html_desc()}</a>')
return format_html('<br>'.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'<a href="{url}">{activity.html_desc()}</a>')
return format_html('<br>'.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',)

@ -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

@ -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()
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 = '<table><tr>'
for field in fields:
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>
<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:cleanup' %}" class="addlink" style="margin-right: 5px;">Reset</a>
<!-- <a href="{% url 'admin:cleanup' %}" class="deletelink" style="margin-right: 5px;">Reset</a> -->
</li>
{% 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/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')),

Loading…
Cancel
Save