parent
09620693e8
commit
ff0fb01246
@ -1,96 +1,455 @@ |
||||
from django.http import HttpResponseRedirect |
||||
from django.contrib import admin |
||||
from django.urls import path, reverse |
||||
from django.contrib import messages |
||||
from django.shortcuts import render, redirect |
||||
from django.contrib.auth import get_user_model |
||||
from django.utils.html import format_html |
||||
from .models import ( |
||||
Prospect, |
||||
Status, |
||||
ProspectStatus, |
||||
Event, |
||||
EmailCampaign, |
||||
EmailTracker |
||||
) |
||||
|
||||
import csv |
||||
import io |
||||
import time |
||||
|
||||
from .models import Entity, Prospect, Activity, Status, ActivityType, EmailTemplate |
||||
from .forms import FileImportForm, EmailTemplateSelectionForm |
||||
from .filters import StaffUserFilter, ProspectProfileFilter |
||||
|
||||
from tournaments.models import CustomUser |
||||
from tournaments.models.enums import UserOrigin |
||||
from sync.admin import SyncedObjectAdmin |
||||
|
||||
User = get_user_model() |
||||
|
||||
@admin.register(Entity) |
||||
class EntityAdmin(SyncedObjectAdmin): |
||||
list_display = ('name', 'address', 'zip_code', 'city') |
||||
search_fields = ('name', 'address', 'zip_code', 'city') |
||||
|
||||
@admin.register(EmailTemplate) |
||||
class EmailTemplateAdmin(SyncedObjectAdmin): |
||||
list_display = ('name', 'subject', 'body') |
||||
search_fields = ('name', 'subject') |
||||
|
||||
def contacted_by_sms(modeladmin, request, queryset): |
||||
create_activity_for_prospect(modeladmin, request, queryset, None, Status.CONTACTED) |
||||
contacted_by_sms.short_description = "Contacted by SMS" |
||||
|
||||
def mark_as_customer(modeladmin, request, queryset): |
||||
create_activity_for_prospect(modeladmin, request, queryset, None, Status.CUSTOMER) |
||||
mark_as_customer.short_description = "Mark as customer" |
||||
|
||||
def mark_as_should_test(modeladmin, request, queryset): |
||||
create_activity_for_prospect(modeladmin, request, queryset, None, Status.TESTING) |
||||
mark_as_should_test.short_description = "Mark as should test" |
||||
|
||||
def mark_as_testing(modeladmin, request, queryset): |
||||
create_activity_for_prospect(modeladmin, request, queryset, None, Status.CUSTOMER) |
||||
mark_as_testing.short_description = "Mark as testing" |
||||
|
||||
def declined_too_expensive(modeladmin, request, queryset): |
||||
create_activity_for_prospect(modeladmin, request, queryset, None, Status.DECLINED_TOO_EXPENSIVE) |
||||
declined_too_expensive.short_description = "Declined too expensive" |
||||
|
||||
def declined_use_something_else(modeladmin, request, queryset): |
||||
create_activity_for_prospect(modeladmin, request, queryset, None, Status.DECLINED_USE_SOMETHING_ELSE) |
||||
declined_use_something_else.short_description = "Declined use something else" |
||||
|
||||
def create_activity_for_prospect(modeladmin, request, queryset, type, status): |
||||
for prospect in queryset: |
||||
activity = Activity.objects.create( |
||||
type=type, |
||||
status=status, |
||||
related_user = request.user |
||||
) |
||||
activity.prospects.add(prospect) |
||||
|
||||
modeladmin.message_user( |
||||
request, |
||||
f'{queryset.count()} prospects were marked as {status}.' |
||||
) |
||||
|
||||
def create_activity_for_prospect(modeladmin, request, queryset): |
||||
# Only allow single selection |
||||
if queryset.count() != 1: |
||||
messages.error(request, "Please select exactly one prospect.") |
||||
return |
||||
|
||||
prospect = queryset.first() |
||||
|
||||
# Build the URL with pre-populated fields |
||||
url = reverse('admin:crm_activity_add') |
||||
url += f'?prospect={prospect.id}' |
||||
|
||||
# You can add more fields as URL parameters |
||||
# url += f'&title=Event for {prospect.name}&status=pending' |
||||
|
||||
return redirect(url) |
||||
create_activity_for_prospect.short_description = "Create event" |
||||
|
||||
@admin.register(Prospect) |
||||
class ProspectAdmin(admin.ModelAdmin): |
||||
list_display = ('entity_name', 'first_name', 'last_name', 'email', 'address', 'zip_code', 'city', 'created_at') |
||||
list_filter = ('zip_code', 'created_at') |
||||
search_fields = ('entity_name', 'first_name', 'last_name', 'email', 'zip_code', 'city') |
||||
filter_horizontal = ('users',) |
||||
date_hierarchy = 'created_at' |
||||
|
||||
@admin.register(Status) |
||||
class StatusAdmin(admin.ModelAdmin): |
||||
list_display = ('name', 'created_at') |
||||
search_fields = ('name',) |
||||
|
||||
@admin.register(ProspectStatus) |
||||
class ProspectStatusAdmin(admin.ModelAdmin): |
||||
list_display = ('prospect', 'status', 'created_at') |
||||
list_filter = ('status', 'created_at') |
||||
search_fields = ('prospect__name', 'prospect__email') |
||||
date_hierarchy = 'created_at' |
||||
|
||||
@admin.register(Event) |
||||
class EventAdmin(admin.ModelAdmin): |
||||
list_display = ('get_event_display', 'type', 'date', 'status', 'created_at') |
||||
list_filter = ('type', 'status', 'date') |
||||
search_fields = ('description',) |
||||
filter_horizontal = ('prospects',) |
||||
date_hierarchy = 'date' |
||||
class ProspectAdmin(SyncedObjectAdmin): |
||||
readonly_fields = ['related_events', 'entity_names', 'current_status'] |
||||
fieldsets = [ |
||||
(None, { |
||||
'fields': ['related_events', 'entity_names', 'first_name', 'last_name', 'email', 'phone', 'name_unsure', 'official_user', 'entities'] |
||||
}), |
||||
] |
||||
list_display = ('entity_names', 'first_name', 'last_name', 'email', 'last_update', 'current_status') |
||||
list_filter = ('creation_date', StaffUserFilter, 'source', ProspectProfileFilter) |
||||
search_fields = ('first_name', 'last_name', 'email') |
||||
date_hierarchy = 'creation_date' |
||||
change_list_template = "admin/crm/prospect/change_list.html" |
||||
ordering = ['-last_update'] |
||||
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 get_event_display(self, obj): |
||||
return str(obj) |
||||
get_event_display.short_description = 'Event' |
||||
|
||||
@admin.register(EmailCampaign) |
||||
class EmailCampaignAdmin(admin.ModelAdmin): |
||||
list_display = ('subject', 'event', 'sent_at') |
||||
list_filter = ('sent_at',) |
||||
search_fields = ('subject', 'content') |
||||
date_hierarchy = 'sent_at' |
||||
readonly_fields = ('sent_at',) |
||||
|
||||
@admin.register(EmailTracker) |
||||
class EmailTrackerAdmin(admin.ModelAdmin): |
||||
list_display = ( |
||||
'campaign', |
||||
'prospect', |
||||
'tracking_id', |
||||
'sent_status', |
||||
'opened_status', |
||||
'clicked_status' |
||||
def related_events(self, obj): |
||||
events = obj.events.all() |
||||
if events: |
||||
event_links = [] |
||||
for event in events: |
||||
url = f"/kingdom/crm/event/{event.id}/change/" |
||||
event_links.append(f'<a href="{url}">{event.html_desc()}</a>') |
||||
return format_html('<br>'.join(event_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:crm_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): |
||||
urls = super().get_urls() |
||||
custom_urls = [ |
||||
path('import_file/', self.admin_site.admin_view(self.import_file), name='import_file'), |
||||
path('import_app_users/', self.admin_site.admin_view(self.import_app_users), name='import_app_users'), |
||||
path('cleanup/', self.admin_site.admin_view(self.cleanup), name='cleanup'), |
||||
] |
||||
return custom_urls + urls |
||||
|
||||
def cleanup(self, request): |
||||
Entity.objects.all().delete() |
||||
Prospect.objects.all().delete() |
||||
Activity.objects.all().delete() |
||||
|
||||
messages.success(request, 'cleanup CRM objects') |
||||
return redirect('admin:crm_prospect_changelist') |
||||
|
||||
def import_app_users(self, request): |
||||
users = CustomUser.objects.filter(origin=UserOrigin.APP) |
||||
|
||||
created_count = 0 |
||||
for user in users: |
||||
is_customer = user.purchases.count() > 0 |
||||
entity_name = user.latest_event_club_name() |
||||
|
||||
prospect, prospect_created = Prospect.objects.get_or_create( |
||||
email=user.email, |
||||
defaults={ |
||||
'first_name': user.first_name, |
||||
'last_name': user.last_name, |
||||
'phone': user.phone, |
||||
'name_unsure': False, |
||||
'official_user': user, |
||||
'source': 'App', |
||||
} |
||||
) |
||||
if entity_name: |
||||
entity, entity_created = Entity.objects.get_or_create( |
||||
name=entity_name, |
||||
defaults={'name': entity_name} |
||||
) |
||||
prospect.entities.add(entity) |
||||
|
||||
if is_customer: |
||||
activity = Activity.objects.create( |
||||
status=Status.CUSTOMER, |
||||
) |
||||
activity.prospects.add(prospect) |
||||
if prospect_created: |
||||
created_count += 1 |
||||
|
||||
messages.success(request, f'Imported {created_count} app users into prospects') |
||||
return redirect('admin:crm_prospect_changelist') |
||||
|
||||
def import_file(self, request): |
||||
""" |
||||
Handle file import - displays form and processes file upload |
||||
""" |
||||
if request.method == 'POST': |
||||
form = FileImportForm(request.POST, request.FILES) |
||||
if form.is_valid(): |
||||
# Call the import_csv method with the uploaded file |
||||
try: |
||||
result = self.import_csv(form.cleaned_data['file'], form.cleaned_data['source']) |
||||
messages.success(request, f'File imported successfully: {result}') |
||||
return redirect('admin:crm_prospect_changelist') |
||||
except Exception as e: |
||||
messages.error(request, f'Error importing file: {str(e)}') |
||||
else: |
||||
messages.error(request, 'Please correct the errors below.') |
||||
else: |
||||
form = FileImportForm() |
||||
|
||||
context = { |
||||
'form': form, |
||||
'title': 'Import File', |
||||
'app_label': self.model._meta.app_label, |
||||
'opts': self.model._meta, |
||||
'has_change_permission': self.has_change_permission(request), |
||||
} |
||||
return render(request, 'admin/crm/prospect/import_file.html', context) |
||||
|
||||
def import_csv(self, file, source): |
||||
""" |
||||
Process the uploaded CSV file |
||||
CSV format: entity_name,last_name,first_name,email,phone,attachment_text,status,related_user |
||||
""" |
||||
try: |
||||
# Read the file content |
||||
file_content = file.read().decode('utf-8') |
||||
csv_reader = csv.reader(io.StringIO(file_content)) |
||||
|
||||
created_prospects = 0 |
||||
updated_prospects = 0 |
||||
created_entities = 0 |
||||
created_events = 0 |
||||
|
||||
for row in csv_reader: |
||||
if len(row) < 8: |
||||
continue # Skip rows that don't have enough columns |
||||
|
||||
entity_name = row[0].strip() |
||||
last_name = row[1].strip() |
||||
first_name = row[2].strip() |
||||
email = row[3].strip() |
||||
phone = row[4].strip() if row[4].strip() else None |
||||
if phone and not phone.startswith('0'): |
||||
phone = '0' + phone |
||||
attachment_text = row[5].strip() if row[5].strip() else None |
||||
status_text = row[6].strip() if row[6].strip() else None |
||||
related_user_name = row[7].strip() if row[7].strip() else None |
||||
|
||||
# Create or get Entity |
||||
entity = None |
||||
if entity_name: |
||||
entity, entity_created = Entity.objects.get_or_create( |
||||
name=entity_name, |
||||
defaults={'name': entity_name} |
||||
) |
||||
list_filter = ('sent', 'opened', 'clicked') |
||||
search_fields = ( |
||||
'prospect__name', |
||||
'prospect__email', |
||||
'campaign__subject' |
||||
if entity_created: |
||||
created_entities += 1 |
||||
|
||||
# Get related user if provided |
||||
related_user = None |
||||
if related_user_name: |
||||
try: |
||||
related_user = User.objects.get(username=related_user_name) |
||||
except User.DoesNotExist: |
||||
# Try to find by first name if username doesn't exist |
||||
related_user = User.objects.filter(first_name__icontains=related_user_name).first() |
||||
|
||||
# Create or update Prospect |
||||
prospect, prospect_created = Prospect.objects.get_or_create( |
||||
email=email, |
||||
defaults={ |
||||
'first_name': first_name, |
||||
'last_name': last_name, |
||||
'phone': phone, |
||||
'name_unsure': False, |
||||
'related_user': related_user, |
||||
'source': source, |
||||
} |
||||
) |
||||
readonly_fields = ( |
||||
'tracking_id', 'sent', 'sent_at', |
||||
'opened', 'opened_at', |
||||
'clicked', 'clicked_at' |
||||
|
||||
if prospect_created: |
||||
created_prospects += 1 |
||||
else: |
||||
# Check if names are different and mark as name_unsure |
||||
if (prospect.first_name != first_name or prospect.last_name != last_name): |
||||
prospect.name_unsure = True |
||||
# Update related_user if provided |
||||
if related_user: |
||||
prospect.related_user = related_user |
||||
prospect.save() |
||||
updated_prospects += 1 |
||||
|
||||
# Associate entity with prospect |
||||
if entity: |
||||
prospect.entities.add(entity) |
||||
|
||||
# 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 |
||||
if status_text: |
||||
if 'CONTACTED' in status_text: |
||||
status_value = Status.CONTACTED |
||||
elif 'RESPONDED' in status_text: |
||||
status_value = Status.RESPONDED |
||||
elif 'SHOULD_TEST' in status_text: |
||||
status_value = Status.SHOULD_TEST |
||||
elif 'CUSTOMER' in status_text: |
||||
status_value = Status.CUSTOMER |
||||
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_UNRELATED |
||||
|
||||
activity = Activity.objects.create( |
||||
type=ActivityType.SMS, |
||||
attachment_text=attachment_text, |
||||
status=status_value, |
||||
description=f"Imported from CSV - Status: {status_text}" if status_text else "Imported from CSV" |
||||
) |
||||
date_hierarchy = 'sent_at' |
||||
|
||||
def sent_status(self, obj): |
||||
return self._get_status_html(obj.sent, obj.sent_at) |
||||
sent_status.short_description = 'Sent' |
||||
sent_status.allow_tags = True |
||||
|
||||
def opened_status(self, obj): |
||||
return self._get_status_html(obj.opened, obj.opened_at) |
||||
opened_status.short_description = 'Opened' |
||||
opened_status.allow_tags = True |
||||
|
||||
def clicked_status(self, obj): |
||||
return self._get_status_html(obj.clicked, obj.clicked_at) |
||||
clicked_status.short_description = 'Clicked' |
||||
clicked_status.allow_tags = True |
||||
|
||||
def _get_status_html(self, status, date): |
||||
if status: |
||||
return format_html( |
||||
'<span style="color: green;">✓</span> {}', |
||||
date.strftime('%Y-%m-%d %H:%M') if date else '' |
||||
activity.prospects.add(prospect) |
||||
created_events += 1 |
||||
|
||||
result = f"Successfully imported: {created_prospects} new prospects, {updated_prospects} updated prospects, {created_entities} new entities, {created_events} new events" |
||||
return result |
||||
|
||||
except Exception as e: |
||||
raise Exception(f"Error processing CSV file: {str(e)}") |
||||
|
||||
def send_email(self, request, queryset): |
||||
|
||||
if 'apply' in request.POST: |
||||
form = EmailTemplateSelectionForm(request.POST) |
||||
if form.is_valid(): |
||||
email_template = form.cleaned_data['email_template'] |
||||
|
||||
self.process_selected_items_with_template(request, queryset, email_template) |
||||
|
||||
self.message_user(request, f"Email sent to {queryset.count()} prospects using the '{email_template.name}' template.", messages.SUCCESS) |
||||
return HttpResponseRedirect(request.get_full_path()) |
||||
else: |
||||
form = EmailTemplateSelectionForm() |
||||
|
||||
return render(request, 'admin/crm/select_email_template.html', { |
||||
'prospects': queryset, |
||||
'form': form, |
||||
'title': 'Send Email to Prospects' |
||||
}) |
||||
send_email.short_description = "Send email" |
||||
|
||||
def process_selected_items_with_template(self, request, queryset, email_template): |
||||
|
||||
sent_count = 0 |
||||
error_emails = [] |
||||
all_emails = [] |
||||
|
||||
for prospect in queryset: |
||||
mail_body = email_template.body.replace('{{name}}', prospect.first_name) |
||||
all_emails.append(prospect.email) |
||||
|
||||
try: |
||||
send_mail( |
||||
email_template.subject, |
||||
mail_body, |
||||
request.user.email, |
||||
[prospect.email], |
||||
fail_silently=False, |
||||
) |
||||
return format_html('<span style="color: red;">✗</span>') |
||||
sent_count += 1 |
||||
except Exception as e: |
||||
error_emails.append(prospect.email) |
||||
|
||||
time.sleep(1) |
||||
|
||||
|
||||
@admin.register(Activity) |
||||
class ActivityAdmin(SyncedObjectAdmin): |
||||
list_display = ('creation_date', 'type', 'description', 'attachment_text') |
||||
list_filter = ('status', 'type') |
||||
search_fields = ('description',) |
||||
filter_horizontal = ('prospects',) |
||||
date_hierarchy = 'creation_date' |
||||
|
||||
def get_form(self, request, obj=None, **kwargs): |
||||
form = super().get_form(request, obj, **kwargs) |
||||
|
||||
# Pre-populate fields from URL parameters |
||||
if 'prospect' in request.GET: |
||||
try: |
||||
prospect_id = request.GET['prospect'] |
||||
prospect = Prospect.objects.get(id=prospect_id) |
||||
form.base_fields['prospects'].initial = prospect |
||||
form.base_fields['related_user'].initial = request.user |
||||
|
||||
# You can set other fields based on the prospect |
||||
# form.base_fields['title'].initial = f"Event for {prospect.}" |
||||
# form.base_fields['status'].initial = 'pending' |
||||
|
||||
except (Prospect.DoesNotExist, ValueError): |
||||
pass |
||||
|
||||
return form |
||||
|
||||
def get_event_display(self, obj): |
||||
return str(obj) |
||||
get_event_display.short_description = 'Activity' |
||||
|
||||
# @admin.register(EmailCampaign) |
||||
# class EmailCampaignAdmin(admin.ModelAdmin): |
||||
# list_display = ('subject', 'event', 'sent_at') |
||||
# list_filter = ('sent_at',) |
||||
# search_fields = ('subject', 'content') |
||||
# date_hierarchy = 'sent_at' |
||||
# readonly_fields = ('sent_at',) |
||||
|
||||
# @admin.register(EmailTracker) |
||||
# class EmailTrackerAdmin(admin.ModelAdmin): |
||||
# list_display = ( |
||||
# 'campaign', |
||||
# 'prospect', |
||||
# 'tracking_id', |
||||
# 'sent_status', |
||||
# 'opened_status', |
||||
# 'clicked_status' |
||||
# ) |
||||
# list_filter = ('sent', 'opened', 'clicked') |
||||
# search_fields = ( |
||||
# 'prospect__name', |
||||
# 'prospect__email', |
||||
# 'campaign__subject' |
||||
# ) |
||||
# readonly_fields = ( |
||||
# 'tracking_id', 'sent', 'sent_at', |
||||
# 'opened', 'opened_at', |
||||
# 'clicked', 'clicked_at' |
||||
# ) |
||||
# date_hierarchy = 'sent_at' |
||||
|
||||
# def sent_status(self, obj): |
||||
# return self._get_status_html(obj.sent, obj.sent_at) |
||||
# sent_status.short_description = 'Sent' |
||||
# sent_status.allow_tags = True |
||||
|
||||
# def opened_status(self, obj): |
||||
# return self._get_status_html(obj.opened, obj.opened_at) |
||||
# opened_status.short_description = 'Opened' |
||||
# opened_status.allow_tags = True |
||||
|
||||
# def clicked_status(self, obj): |
||||
# return self._get_status_html(obj.clicked, obj.clicked_at) |
||||
# clicked_status.short_description = 'Clicked' |
||||
# clicked_status.allow_tags = True |
||||
|
||||
# def _get_status_html(self, status, date): |
||||
# if status: |
||||
# return format_html( |
||||
# '<span style="color: green;">✓</span> {}', |
||||
# date.strftime('%Y-%m-%d %H:%M') if date else '' |
||||
# ) |
||||
# return format_html('<span style="color: red;">✗</span>') |
||||
|
||||
@ -0,0 +1,70 @@ |
||||
from django.urls import path |
||||
from django.http import HttpResponse |
||||
from tournaments.models import CustomUser |
||||
from tournaments.models.enums import UserOrigin |
||||
from django.core.mail import send_mail |
||||
|
||||
import time |
||||
|
||||
def users_list(with_tournaments): |
||||
return CustomUser.objects.filter(origin=UserOrigin.APP).exclude(purchase__isnull=False).filter(events__isnull=with_tournaments) |
||||
|
||||
def email_users_with_tournaments_count(request): |
||||
users = users_list(False) |
||||
emails = [user.email for user in users] |
||||
return HttpResponse(f'users = {len(users)}, \n\nemails = {emails}') |
||||
|
||||
def email_users_count(request): |
||||
users = users_list(True) |
||||
emails = [user.email for user in users] |
||||
return HttpResponse(f'users = {len(users)}, \n\nemails = {emails}') |
||||
|
||||
def email_users_view(request): |
||||
return email_users(request, users_list(True), 0) |
||||
|
||||
def email_users_with_tournaments(request): |
||||
return email_users(request, users_list(False), 1) |
||||
|
||||
def email_users(request, users, template_index): |
||||
|
||||
users = users_list(True) |
||||
|
||||
subject = 'check Padel Club' |
||||
from_email = 'laurent@padelclub.app' |
||||
|
||||
sent_count = 0 |
||||
error_emails = [] |
||||
all_emails = [] |
||||
|
||||
for user in users: |
||||
mail_body = template(user, template_index) # f'Bonjour {user.first_name}, cool la vie ?' |
||||
all_emails.append(user.email) |
||||
|
||||
try: |
||||
send_mail( |
||||
subject, |
||||
mail_body, |
||||
from_email, |
||||
[user.email], |
||||
fail_silently=False, |
||||
) |
||||
sent_count += 1 |
||||
except Exception as e: |
||||
error_emails.append(user.email) |
||||
|
||||
time.sleep(1) |
||||
|
||||
return HttpResponse(f'users = {len(users)}, sent = {sent_count}, errors = {len(error_emails)}, \n\nemails = {all_emails}, \n\nerror emails = {error_emails}') |
||||
|
||||
def template(user, index): |
||||
if index == 0: |
||||
return f'Bonjour {user.first_name}, \n\n' |
||||
else: |
||||
return f'Bonjour {user.first_name}, \n\nJe te remercie d\'avoir téléchargé Padel Club. J\'ai pu voir que tu avais créé quelques tournois mais sans aller plus loin, est-ce que tu pourrais me dire ce qui t\'as freiné ?\n\nLaurent Morvillier' |
||||
|
||||
urlpatterns = [ |
||||
path('email_users/', email_users_view, name='crm_email_users'), |
||||
path('email_users_count/', email_users_count, name='crm_email_count'), |
||||
path('email_users_with_tournaments_count/', email_users_with_tournaments_count, name='crm_email_with_tournaments_count'), |
||||
path('email_users_with_tournaments/', email_users_with_tournaments, name='email_users_with_tournaments'), |
||||
] |
||||
@ -1,46 +1,61 @@ |
||||
from django import forms |
||||
from .models import Prospect, Event |
||||
import datetime |
||||
|
||||
class SmallTextArea(forms.Textarea): |
||||
def __init__(self, *args, **kwargs): |
||||
kwargs.setdefault('attrs', {}) |
||||
kwargs['attrs'].update({ |
||||
'rows': 2, |
||||
'cols': 100, |
||||
'style': 'height: 80px; width: 800px;' |
||||
}) |
||||
super().__init__(*args, **kwargs) |
||||
|
||||
class ProspectForm(forms.ModelForm): |
||||
class Meta: |
||||
model = Prospect |
||||
fields = ['entity_name', 'first_name', 'last_name', 'email', |
||||
'phone', 'address', 'zip_code', 'city'] |
||||
|
||||
class BulkEmailForm(forms.Form): |
||||
prospects = forms.ModelMultipleChoiceField( |
||||
queryset=Prospect.objects.all(), |
||||
widget=forms.CheckboxSelectMultiple |
||||
) |
||||
subject = forms.CharField(max_length=200) |
||||
content = forms.CharField(widget=forms.Textarea) |
||||
|
||||
class EventForm(forms.ModelForm): |
||||
prospects = forms.ModelMultipleChoiceField( |
||||
queryset=Prospect.objects.all(), |
||||
widget=forms.SelectMultiple(attrs={'class': 'select2'}), |
||||
required=False |
||||
) |
||||
description = forms.CharField(widget=SmallTextArea) |
||||
attachment_text = forms.CharField(widget=SmallTextArea) |
||||
|
||||
class Meta: |
||||
model = Event |
||||
fields = ['date', 'type', 'description', 'attachment_text', 'prospects', 'status'] |
||||
widgets = { |
||||
'date': forms.DateTimeInput(attrs={'type': 'datetime-local'}), |
||||
} |
||||
from .models import EmailTemplate |
||||
|
||||
# class SmallTextArea(forms.Textarea): |
||||
# def __init__(self, *args, **kwargs): |
||||
# kwargs.setdefault('attrs', {}) |
||||
# kwargs['attrs'].update({ |
||||
# 'rows': 2, |
||||
# 'cols': 100, |
||||
# 'style': 'height: 80px; width: 800px;' |
||||
# }) |
||||
# super().__init__(*args, **kwargs) |
||||
|
||||
# class ProspectForm(forms.ModelForm): |
||||
# class Meta: |
||||
# model = Prospect |
||||
# fields = ['entity_name', 'first_name', 'last_name', 'email', |
||||
# 'phone', 'address', 'zip_code', 'city'] |
||||
|
||||
# class BulkEmailForm(forms.Form): |
||||
# prospects = forms.ModelMultipleChoiceField( |
||||
# queryset=Prospect.objects.all(), |
||||
# widget=forms.CheckboxSelectMultiple |
||||
# ) |
||||
# subject = forms.CharField(max_length=200) |
||||
# content = forms.CharField(widget=forms.Textarea) |
||||
|
||||
# class EventForm(forms.ModelForm): |
||||
# prospects = forms.ModelMultipleChoiceField( |
||||
# queryset=Prospect.objects.all(), |
||||
# widget=forms.SelectMultiple(attrs={'class': 'select2'}), |
||||
# required=False |
||||
# ) |
||||
# description = forms.CharField(widget=SmallTextArea) |
||||
# attachment_text = forms.CharField(widget=SmallTextArea) |
||||
|
||||
# class Meta: |
||||
# model = Event |
||||
# fields = ['date', 'type', 'description', 'attachment_text', 'prospects', 'status'] |
||||
# widgets = { |
||||
# 'date': forms.DateTimeInput(attrs={'type': 'datetime-local'}), |
||||
# } |
||||
|
||||
class FileImportForm(forms.Form): |
||||
source = forms.CharField(max_length=200) |
||||
file = forms.FileField( |
||||
label='Select file to import', |
||||
help_text='Choose a file to upload and process', |
||||
widget=forms.FileInput(attrs={'accept': '.csv,.xlsx,.xls,.txt'}) |
||||
) |
||||
|
||||
class CSVImportForm(forms.Form): |
||||
csv_file = forms.FileField() |
||||
|
||||
class EmailTemplateSelectionForm(forms.Form): |
||||
email_template = forms.ModelChoiceField( |
||||
queryset=EmailTemplate.objects.all(), |
||||
empty_label="Select an email template...", |
||||
widget=forms.Select(attrs={'class': 'form-control'}) |
||||
) |
||||
|
||||
@ -1,60 +0,0 @@ |
||||
# Generated by Django 4.2.11 on 2024-12-08 20:58 |
||||
|
||||
from django.conf import settings |
||||
from django.db import migrations, models |
||||
import django.db.models.deletion |
||||
import django.utils.timezone |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
||||
('crm', '0001_initial'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AlterModelOptions( |
||||
name='event', |
||||
options={'ordering': ['-created_at'], 'permissions': [('manage_events', 'Can manage events'), ('view_events', 'Can view events')]}, |
||||
), |
||||
migrations.AlterModelOptions( |
||||
name='prospect', |
||||
options={'permissions': [('manage_prospects', 'Can manage prospects'), ('view_prospects', 'Can view prospects')]}, |
||||
), |
||||
migrations.AddField( |
||||
model_name='event', |
||||
name='created_by', |
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_events', to=settings.AUTH_USER_MODEL), |
||||
), |
||||
migrations.AddField( |
||||
model_name='event', |
||||
name='modified_at', |
||||
field=models.DateTimeField(auto_now=True), |
||||
), |
||||
migrations.AddField( |
||||
model_name='event', |
||||
name='modified_by', |
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modified_events', to=settings.AUTH_USER_MODEL), |
||||
), |
||||
migrations.AddField( |
||||
model_name='prospect', |
||||
name='created_by', |
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_prospects', to=settings.AUTH_USER_MODEL), |
||||
), |
||||
migrations.AddField( |
||||
model_name='prospect', |
||||
name='modified_at', |
||||
field=models.DateTimeField(auto_now=True), |
||||
), |
||||
migrations.AddField( |
||||
model_name='prospect', |
||||
name='modified_by', |
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modified_prospects', to=settings.AUTH_USER_MODEL), |
||||
), |
||||
migrations.AlterField( |
||||
model_name='event', |
||||
name='date', |
||||
field=models.DateTimeField(default=django.utils.timezone.now), |
||||
), |
||||
] |
||||
@ -1,32 +0,0 @@ |
||||
# Generated by Django 5.1 on 2024-12-16 15:43 |
||||
|
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('crm', '0002_alter_event_options_alter_prospect_options_and_more'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.RemoveField( |
||||
model_name='prospect', |
||||
name='region', |
||||
), |
||||
migrations.AddField( |
||||
model_name='prospect', |
||||
name='address', |
||||
field=models.CharField(blank=True, max_length=200, null=True), |
||||
), |
||||
migrations.AddField( |
||||
model_name='prospect', |
||||
name='city', |
||||
field=models.CharField(blank=True, max_length=500, null=True), |
||||
), |
||||
migrations.AddField( |
||||
model_name='prospect', |
||||
name='zip_code', |
||||
field=models.CharField(blank=True, max_length=20, null=True), |
||||
), |
||||
] |
||||
@ -1,32 +0,0 @@ |
||||
# Generated by Django 5.1 on 2024-12-16 16:24 |
||||
|
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('crm', '0003_remove_prospect_region_prospect_address_and_more'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.RemoveField( |
||||
model_name='prospect', |
||||
name='name', |
||||
), |
||||
migrations.AddField( |
||||
model_name='prospect', |
||||
name='entity_name', |
||||
field=models.CharField(blank=True, max_length=200, null=True), |
||||
), |
||||
migrations.AddField( |
||||
model_name='prospect', |
||||
name='first_name', |
||||
field=models.CharField(blank=True, max_length=200, null=True), |
||||
), |
||||
migrations.AddField( |
||||
model_name='prospect', |
||||
name='last_name', |
||||
field=models.CharField(blank=True, max_length=200, null=True), |
||||
), |
||||
] |
||||
@ -1,18 +0,0 @@ |
||||
# Generated by Django 5.1 on 2024-12-16 16:49 |
||||
|
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('crm', '0004_remove_prospect_name_prospect_entity_name_and_more'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AddField( |
||||
model_name='prospect', |
||||
name='phone', |
||||
field=models.CharField(blank=True, max_length=25, null=True), |
||||
), |
||||
] |
||||
@ -0,0 +1,17 @@ |
||||
from rest_framework import serializers |
||||
from .models import Activity, Prospect, Entity |
||||
|
||||
class CRMActivitySerializer(serializers.ModelSerializer): |
||||
class Meta: |
||||
model = Activity |
||||
fields = '__all__' |
||||
|
||||
class CRMProspectSerializer(serializers.ModelSerializer): |
||||
class Meta: |
||||
model = Prospect |
||||
fields = '__all__' |
||||
|
||||
class CRMEntitySerializer(serializers.ModelSerializer): |
||||
class Meta: |
||||
model = Entity |
||||
fields = '__all__' |
||||
@ -0,0 +1,25 @@ |
||||
<!-- templates/admin/crm/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 'crm_email_count' %}" class="button default" style="margin-right: 5px;"> |
||||
{% trans "Count Users no event" %} |
||||
</a> |
||||
<a href="{% url 'crm_email_users' %}" class="button default" style="margin-right: 5px;"> |
||||
{% trans "Insta send email no event" %} |
||||
</a> |
||||
<a href="{% url 'crm_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 %} |
||||
@ -0,0 +1,81 @@ |
||||
{% extends "admin/base_site.html" %} |
||||
{% load i18n admin_urls static admin_list %} |
||||
|
||||
{% block title %}Email Users{% endblock %} |
||||
|
||||
{% block breadcrumbs %} |
||||
<div class="breadcrumbs"> |
||||
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a> |
||||
› Email Users |
||||
</div> |
||||
{% endblock %} |
||||
|
||||
{% block content %} |
||||
<div class="module filtered"> |
||||
<h2>Filter Users for Email</h2> |
||||
|
||||
<form method="post" action="{% url 'admin:email_users' %}"> |
||||
{% csrf_token %} |
||||
|
||||
<div class="form-row"> |
||||
<div class="field-box"> |
||||
<label for="user_origin">User Origin:</label> |
||||
<select name="user_origin" id="user_origin" class="vTextField"> |
||||
<option value="">All Origins</option> |
||||
{% for choice in user_origin_choices %} |
||||
<option value="{{ choice.0 }}" {% if choice.0 == selected_origin %}selected{% endif %}> |
||||
{{ choice.1 }} |
||||
</option> |
||||
{% endfor %} |
||||
</select> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="form-row"> |
||||
<div class="field-box"> |
||||
<label for="has_purchase"> |
||||
<input type="checkbox" name="has_purchase" id="has_purchase" value="1" |
||||
{% if has_purchase %}checked{% endif %}> |
||||
User has made a purchase |
||||
</label> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="form-row"> |
||||
<input type="submit" value="Filter Users" class="default" name="_filter"> |
||||
</div> |
||||
</form> |
||||
|
||||
{% if filtered_users %} |
||||
<div class="results"> |
||||
<h3>Filtered Users ({{ filtered_users|length }} found)</h3> |
||||
<div class="module"> |
||||
<table cellspacing="0"> |
||||
<thead> |
||||
<tr> |
||||
<th>Email</th> |
||||
<th>Origin</th> |
||||
<th>Has Purchase</th> |
||||
<th>Date Joined</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{% for user in filtered_users %} |
||||
<tr class="{% cycle 'row1' 'row2' %}"> |
||||
<td>{{ user.email }}</td> |
||||
<td>{{ user.get_origin_display }}</td> |
||||
<td>{{ user.has_purchase|yesno:"Yes,No" }}</td> |
||||
<td>{{ user.date_joined|date:"M d, Y" }}</td> |
||||
</tr> |
||||
{% empty %} |
||||
<tr> |
||||
<td colspan="4">No users found matching criteria.</td> |
||||
</tr> |
||||
{% endfor %} |
||||
</tbody> |
||||
</table> |
||||
</div> |
||||
</div> |
||||
{% endif %} |
||||
</div> |
||||
{% endblock %} |
||||
@ -0,0 +1,22 @@ |
||||
{% extends "admin/change_list.html" %} |
||||
|
||||
{% block object-tools-items %} |
||||
{{ block.super }} |
||||
<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> |
||||
</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 %} |
||||
@ -0,0 +1,53 @@ |
||||
<!-- templates/admin/import_file.html --> |
||||
{% extends "admin/base_site.html" %} |
||||
{% load i18n static %} |
||||
|
||||
{% block title %}{% trans 'Import File' %}{% endblock %} |
||||
|
||||
{% block breadcrumbs %} |
||||
<div class="breadcrumbs"> |
||||
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a> |
||||
› {% trans 'Import File' %} |
||||
</div> |
||||
{% endblock %} |
||||
|
||||
{% block content %} |
||||
<div class="module"> |
||||
<form method="post" enctype="multipart/form-data" novalidate> |
||||
{% csrf_token %} |
||||
|
||||
<div class="form-row"> |
||||
<div class="field-box"> |
||||
{{ form.source.label_tag }} |
||||
{{ form.source }} |
||||
</div> |
||||
|
||||
<div class="field-box"> |
||||
{{ form.file.label_tag }} |
||||
{{ form.file }} |
||||
{% if form.file.help_text %} |
||||
<div class="help">{{ form.file.help_text }}</div> |
||||
{% endif %} |
||||
{% if form.file.errors %} |
||||
<ul class="errorlist"> |
||||
{% for error in form.file.errors %} |
||||
<li>{{ error }}</li> |
||||
{% endfor %} |
||||
</ul> |
||||
{% endif %} |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="submit-row"> |
||||
<input type="submit" value="{% trans 'Import File' %}" class="default" /> |
||||
<a href="{% url 'admin:index' %}" class="button cancel-link">{% trans 'Cancel' %}</a> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
|
||||
<div class="module"> |
||||
<h2>{% trans 'Instructions' %}</h2> |
||||
<p>{% trans 'Select a file to import and click "Import File" to process it.' %}</p> |
||||
<p>{% trans 'Supported file formats: CSV, Excel (XLSX, XLS), and Text files.' %}</p> |
||||
</div> |
||||
{% endblock %} |
||||
@ -0,0 +1,29 @@ |
||||
{% extends "admin/base_site.html" %} |
||||
{% load i18n admin_urls %} |
||||
|
||||
{% block content %} |
||||
<div id="content-main"> |
||||
<form action="" method="post"> |
||||
{% csrf_token %} |
||||
<h2>{{ title }}</h2> |
||||
|
||||
<p>You have selected the following prospects:</p> |
||||
<ul> |
||||
{% for prospect in prospects %} |
||||
<li>{{ prospect.name }} ({{ prospect.email }})</li> |
||||
<input type="hidden" name="_selected_action" value="{{ prospect.pk }}" /> |
||||
{% endfor %} |
||||
</ul> |
||||
|
||||
<fieldset class="module aligned"> |
||||
<h2>Select an email template:</h2> |
||||
{{ form.as_p }} |
||||
</fieldset> |
||||
|
||||
<div class="submit-row"> |
||||
<input type="hidden" name="action" value="send_email" /> |
||||
<input type="submit" name="apply" value="Send Email" class="default" /> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
{% endblock %} |
||||
|
unable to load file from base commit
|
@ -0,0 +1,18 @@ |
||||
# Generated by Django 5.1 on 2025-07-03 10:01 |
||||
|
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('tournaments', '0130_playerregistration_contact_email_and_more'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AlterField( |
||||
model_name='playerregistration', |
||||
name='contact_name', |
||||
field=models.CharField(blank=True, max_length=200, null=True), |
||||
), |
||||
] |
||||
@ -0,0 +1,20 @@ |
||||
# Generated by Django 5.1 on 2025-07-09 12:55 |
||||
|
||||
import django.db.models.deletion |
||||
from django.conf import settings |
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('tournaments', '0131_alter_playerregistration_contact_name'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AlterField( |
||||
model_name='purchase', |
||||
name='user', |
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='purchases', to=settings.AUTH_USER_MODEL), |
||||
), |
||||
] |
||||
Loading…
Reference in new issue