From 09282698cf0c65393096bc492a14bd24a848cadc Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 22 Sep 2025 10:55:28 +0200 Subject: [PATCH 1/7] Adds sharable property to BaseModel classes --- biz/admin.py | 54 -------------------------------------- sync/README.md | 21 +++++++++++++++ sync/admin.py | 2 +- sync/models/base.py | 15 ++++++----- tournaments/admin.py | 2 +- tournaments/models/club.py | 2 ++ 6 files changed, 33 insertions(+), 63 deletions(-) create mode 100644 sync/README.md diff --git a/biz/admin.py b/biz/admin.py index e1a4556..a83003e 100644 --- a/biz/admin.py +++ b/biz/admin.py @@ -429,57 +429,3 @@ class ActivityAdmin(SyncedObjectAdmin): 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( -# ' {}', -# date.strftime('%Y-%m-%d %H:%M') if date else '' -# ) -# return format_html('') diff --git a/sync/README.md b/sync/README.md new file mode 100644 index 0000000..b309631 --- /dev/null +++ b/sync/README.md @@ -0,0 +1,21 @@ +### Synchronization quick ReadMe + +- Data class must extend BaseModel +- Admin classes must extend SyncedObjectAdmin to have updates saved in the BaseModel properties +- The SynchronizationApi defines a get and a post service to POST new data, and GET updates. When performing an operation on a data, a ModelLog instance is created with the related information. When performing a GET, we retrieve the list of ModelLogs to sent the new data to the user. +- routing.py defines the URL of the websocket where messages are sent when updates are made. URL is by user. + + +### Sharing + +- Data can be shared between users. To do that, a new DataAccess object can be created to define the owner, the authorized user, and the object id. +- By default, the whole hierarchy of objects are shared, from the data parents to all its children. +- Special data path can be specified for a class by defining a setting + +example: +SYNC_MODEL_CHILDREN_SHARING = { + 'Match': ['team_scores', 'team_registration', 'player_registrations'] +} +Here when sharing a Match, we also share objects accessed through the names of the properties to get TeamScore, TeamRegistration and PlayerRegistration. + +- It's also possible to exclude a class from being sharable by setting sharable = False in its definition. In PadelClub, Club is the top entity that links all data together, so we don't want the automatic data scanning to share clubs. diff --git a/sync/admin.py b/sync/admin.py index 0f9f713..296df11 100644 --- a/sync/admin.py +++ b/sync/admin.py @@ -5,7 +5,7 @@ from .models import BaseModel, ModelLog, DataAccess class SyncedObjectAdmin(admin.ModelAdmin): - exclude = ('data_access_ids',) + # exclude = ('data_access_ids',) raw_id_fields = ['related_user', 'last_updated_by'] def save_model(self, request, obj, form, change): diff --git a/sync/models/base.py b/sync/models/base.py index 562dd56..564def2 100644 --- a/sync/models/base.py +++ b/sync/models/base.py @@ -16,6 +16,8 @@ class BaseModel(models.Model): last_updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL, related_name='+') data_access_ids = models.JSONField(default=list) + sharable = True + class Meta: abstract = True @@ -39,6 +41,8 @@ class BaseModel(models.Model): } def update_data_access_list(self): + if self.sharable == False: + return related_instances = self.sharing_related_instances() data_access_ids = set() for instance in related_instances: @@ -46,21 +50,18 @@ class BaseModel(models.Model): data_access_ids.update(instance.data_access_ids) # print(f'related_instances = {related_instances}') - # data_access_ids = [instance.data_access_ids for instance in related_instances if isinstance(instance, BaseModel)] - # data_access_ids.extend(self.data_access_ids) self.data_access_ids = list(data_access_ids) - # DataAccess = apps.get_model('sync', 'DataAccess') - # data_accesses = DataAccess.objects.filter(model_id__in=related_ids) - # for data_access in data_accesses: - # self.add_data_access_relation(data_access) - def add_data_access_relation(self, data_access): + if self.sharable == False: + return str_id = str(data_access.id) if str_id not in self.data_access_ids: self.data_access_ids.append(str_id) def remove_data_access_relation(self, data_access): + if self.sharable == False: + return try: self.data_access_ids.remove(str(data_access.id)) except ValueError: diff --git a/tournaments/admin.py b/tournaments/admin.py index 4febf0d..b8fd869 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -95,7 +95,7 @@ class EventAdmin(SyncedObjectAdmin): readonly_fields = ['display_images_preview'] fieldsets = [ - (None, {'fields': ['last_update', 'related_user', 'name', 'club', 'creator', 'creation_date', 'tenup_id']}), + (None, {'fields': ['data_access_ids','last_update', 'related_user', 'name', 'club', 'creator', 'creation_date', 'tenup_id']}), ('Images', {'fields': ['display_images_preview'], 'classes': ['collapse']}), ] diff --git a/tournaments/models/club.py b/tournaments/models/club.py index d0f6b56..3582f51 100644 --- a/tournaments/models/club.py +++ b/tournaments/models/club.py @@ -27,6 +27,8 @@ class Club(BaseModel): broadcast_code = models.CharField(max_length=10, null=True, blank=True, unique=True) admin_visible = models.BooleanField(default=False) + sharable = False + def delete_dependencies(self): for court in self.courts.all(): # court.delete_dependencies() From f1c02a7d1ba26aabb5ff281adf498db2e80196c0 Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 22 Sep 2025 15:01:56 +0200 Subject: [PATCH 2/7] Adds new Campaign model to make groups of users + way of creating them from users --- biz/admin.py | 16 ++++++-- biz/filters.py | 15 +++++++- .../0005_alter_activity_status_campaign.py | 38 +++++++++++++++++++ biz/models.py | 8 ++++ tournaments/admin.py | 31 +++++++++++++-- 5 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 biz/migrations/0005_alter_activity_status_campaign.py diff --git a/biz/admin.py b/biz/admin.py index a83003e..c853ffb 100644 --- a/biz/admin.py +++ b/biz/admin.py @@ -12,9 +12,9 @@ import io import time import logging -from .models import Entity, Prospect, Activity, Status, ActivityType, EmailTemplate, DeclinationReason +from .models import Entity, Prospect, Activity, Status, ActivityType, EmailTemplate, DeclinationReason, Campaign from .forms import FileImportForm, EmailTemplateSelectionForm -from .filters import ContactAgainFilter, ProspectStatusFilter, StaffUserFilter, ProspectProfileFilter, ProspectDeclineReasonFilter +from .filters import ContactAgainFilter, ProspectStatusFilter, StaffUserFilter, ProspectProfileFilter, ProspectDeclineReasonFilter, ProspectCampaignFilter from tournaments.models import CustomUser from tournaments.models.enums import UserOrigin @@ -75,6 +75,10 @@ def declined_android_user(modeladmin, request, queryset): create_default_activity_for_prospect(modeladmin, request, queryset, None, Status.DECLINED, DeclinationReason.USE_ANDROID) declined_android_user.short_description = "Declined use Android" +def mark_as_have_account(modeladmin, request, queryset): + create_default_activity_for_prospect(modeladmin, request, queryset, None, Status.HAVE_CREATED_ACCOUNT, None) +mark_as_have_account.short_description = "Mark as having an account" + def create_default_activity_for_prospect(modeladmin, request, queryset, type, status, reason): for prospect in queryset: activity = Activity.objects.create( @@ -114,13 +118,13 @@ class ProspectAdmin(SyncedObjectAdmin): ] list_display = ('first_name', 'last_name', 'entity_names', 'last_update_date', 'current_status', 'current_text', 'contact_again') - list_filter = (ContactAgainFilter, ProspectStatusFilter, ProspectDeclineReasonFilter, 'creation_date', StaffUserFilter, 'source', ProspectProfileFilter) + list_filter = (ContactAgainFilter, ProspectStatusFilter, ProspectDeclineReasonFilter, ProspectCampaignFilter, 'creation_date', StaffUserFilter, 'source', ProspectProfileFilter) search_fields = ('first_name', 'last_name', 'email', 'entities__name') date_hierarchy = 'creation_date' change_list_template = "admin/biz/prospect/change_list.html" ordering = ['-last_update'] filter_horizontal = ['entities'] - actions = ['send_email', create_activity_for_prospect, mark_as_inbound, contacted_by_sms, mark_as_should_test, mark_as_testing, mark_as_customer, declined_too_expensive, declined_use_something_else, declined_android_user] + actions = ['send_email', create_activity_for_prospect, mark_as_inbound, contacted_by_sms, mark_as_should_test, mark_as_testing, mark_as_customer, mark_as_have_account, declined_too_expensive, declined_use_something_else, declined_android_user] raw_id_fields = ['official_user', 'related_user'] def last_update_date(self, obj): @@ -397,6 +401,10 @@ class ProspectAdmin(SyncedObjectAdmin): time.sleep(1) +@admin.register(Campaign) +class CampaignAdmin(SyncedObjectAdmin): + list_display = ('name', 'user_count') + date_hierarchy = 'creation_date' @admin.register(Activity) class ActivityAdmin(SyncedObjectAdmin): diff --git a/biz/filters.py b/biz/filters.py index 9c3f6a2..6e10e8c 100644 --- a/biz/filters.py +++ b/biz/filters.py @@ -7,7 +7,7 @@ from django.utils import timezone from dateutil.relativedelta import relativedelta -from .models import Activity, Prospect, Status, DeclinationReason +from .models import Activity, Prospect, Status, DeclinationReason, Campaign User = get_user_model() @@ -110,6 +110,19 @@ class ProspectDeclineReasonFilter(admin.SimpleListFilter): else: return queryset +class ProspectCampaignFilter(admin.SimpleListFilter): + title = 'Campaign' + parameter_name = 'campaign' + + def lookups(self, request, model_admin): + campaigns = Campaign.objects.all().order_by('-creation_date') + return [(campaign.id, campaign.name) for campaign in campaigns] + + def queryset(self, request, queryset): + if self.value(): + return queryset.filter(campaigns__id=self.value()) + return queryset + class ContactAgainFilter(admin.SimpleListFilter): title = 'Contact again' # or whatever you want parameter_name = 'contact_again' diff --git a/biz/migrations/0005_alter_activity_status_campaign.py b/biz/migrations/0005_alter_activity_status_campaign.py new file mode 100644 index 0000000..7dfa999 --- /dev/null +++ b/biz/migrations/0005_alter_activity_status_campaign.py @@ -0,0 +1,38 @@ +# Generated by Django 5.1 on 2025-09-22 12:34 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('biz', '0004_prospect_contact_again'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='activity', + name='status', + field=models.CharField(blank=True, choices=[('NONE', 'None'), ('INBOUND', 'Inbound'), ('CONTACTED', 'Contacted'), ('RESPONDED', 'Responded'), ('SHOULD_TEST', 'Should test'), ('TESTING', 'Testing'), ('CUSTOMER', 'Customer'), ('LOST', 'Lost customer'), ('DECLINED', 'Declined'), ('NOT_CONCERNED', 'Not concerned'), ('SHOULD_BUY', 'Should buy'), ('HAVE_CREATED_ACCOUNT', 'Have created account')], max_length=50, null=True), + ), + migrations.CreateModel( + name='Campaign', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('creation_date', models.DateTimeField(default=django.utils.timezone.now, editable=False)), + ('last_update', models.DateTimeField(default=django.utils.timezone.now)), + ('data_access_ids', models.JSONField(default=list)), + ('name', models.CharField(blank=True, max_length=200, null=True)), + ('last_updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('prospects', models.ManyToManyField(blank=True, related_name='campaigns', to='biz.prospect')), + ('related_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/biz/models.py b/biz/models.py index e406541..69401a6 100644 --- a/biz/models.py +++ b/biz/models.py @@ -25,6 +25,7 @@ class Status(models.TextChoices): # DECLINED_UNRELATED = 'DECLINED_UNRELATED', 'Declined without significance' NOT_CONCERNED = 'NOT_CONCERNED', 'Not concerned' SHOULD_BUY = 'SHOULD_BUY', 'Should buy' + HAVE_CREATED_ACCOUNT = 'HAVE_CREATED_ACCOUNT', 'Have created account' class DeclinationReason(models.TextChoices): TOO_EXPENSIVE = 'TOO_EXPENSIVE', 'Too expensive' @@ -170,6 +171,13 @@ class EmailTemplate(BaseModel): def delete_dependencies(self): pass +class Campaign(BaseModel): + name = models.CharField(max_length=200, null=True, blank=True) + prospects = models.ManyToManyField(Prospect, blank=True, related_name='campaigns') + + def user_count(self): + return self.prospects.count() + # class EmailCampaign(models.Model): # event = models.OneToOneField(Event, on_delete=models.CASCADE) # subject = models.CharField(max_length=200) diff --git a/tournaments/admin.py b/tournaments/admin.py index b8fd869..49d013f 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -9,7 +9,7 @@ from django.shortcuts import render from django.db.models import Avg from datetime import timedelta, datetime -from biz.models import Prospect +from biz.models import Prospect, Campaign from .models import Club, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, Court, DateInterval, FailedApiCall, Log, DeviceToken, DrawLog, UnregisteredTeam, UnregisteredPlayer, Image from .forms import CustomUserCreationForm, CustomUserChangeForm @@ -27,7 +27,7 @@ class CustomUserAdmin(UserAdmin): model = CustomUser search_fields = ['username', 'email', 'phone', 'first_name', 'last_name', 'licence_id'] filter_horizontal = ('clubs',) - actions = ['convert_to_prospect'] + actions = ['convert_to_prospect', 'create_campaign'] list_display = ['email', 'first_name', 'last_name', 'username', 'date_joined', 'latest_event_club_name', 'is_active', 'event_count', 'origin', 'registration_payment_mode', 'licence_id'] list_filter = ['is_active', 'origin', UserWithEventsFilter, UserWithPurchasesFilter, UserWithProspectFilter] @@ -59,10 +59,35 @@ class CustomUserAdmin(UserAdmin): obj.last_update = timezone.now() super().save_model(request, obj, form, change) + def create_campaign(self, request, queryset): + prospects = [] + source_value = f"auto_created_{datetime.now().strftime('%Y-%m-%d_%H:%M')}" + for user in queryset: + prospect = Prospect.objects.filter(email=user.email).first() + if prospect: + prospects.append(prospect) + else: + prospect = Prospect.objects.create( + first_name=user.first_name, + last_name=user.last_name, + email=user.email, + phone=user.phone, + official_user=user, + source=source_value + ) + prospects.append(prospect) + campaign = Campaign.objects.create( + name=f"{datetime.now().strftime('%Y-%m-%d_%H:%M')}", + ) + campaign.prospects.add(*prospects) + messages.success(request, f'Created campaign {campaign.name} with {queryset.count()} prospects') + + create_campaign.short_description = "Create campaign with selection" + def convert_to_prospect(self, request, queryset): created_count = 0 skipped_count = 0 - source_value = f"user_conversion_{datetime.now().strftime('%Y%m%d_%H%M')}" + source_value = f"user_conversion_{datetime.now().strftime('%Y-%m-%d_%H:%M')}" for user in queryset: if user.email and Prospect.objects.filter(email=user.email).exists(): From d4de2ae399bb2db959c646ca0884c1c555f577b7 Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 22 Sep 2025 16:02:08 +0200 Subject: [PATCH 3/7] replace int id by uuid id + bonus --- biz/migrations/0006_alter_campaign_id.py | 19 +++++++++++++++++++ biz/models.py | 7 +++++++ 2 files changed, 26 insertions(+) create mode 100644 biz/migrations/0006_alter_campaign_id.py diff --git a/biz/migrations/0006_alter_campaign_id.py b/biz/migrations/0006_alter_campaign_id.py new file mode 100644 index 0000000..9f829d8 --- /dev/null +++ b/biz/migrations/0006_alter_campaign_id.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1 on 2025-09-22 13:10 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('biz', '0005_alter_activity_status_campaign'), + ] + + operations = [ + migrations.AlterField( + model_name='campaign', + name='id', + field=models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False), + ), + ] diff --git a/biz/models.py b/biz/models.py index 69401a6..e271b18 100644 --- a/biz/models.py +++ b/biz/models.py @@ -172,12 +172,19 @@ class EmailTemplate(BaseModel): pass class Campaign(BaseModel): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) name = models.CharField(max_length=200, null=True, blank=True) prospects = models.ManyToManyField(Prospect, blank=True, related_name='campaigns') def user_count(self): return self.prospects.count() + def __str__(self): + return self.name + + def delete_dependencies(self): + pass + # class EmailCampaign(models.Model): # event = models.OneToOneField(Event, on_delete=models.CASCADE) # subject = models.CharField(max_length=200) From 146dae4039dddaaf26541b3547db4a6c156b02af Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 22 Sep 2025 16:02:58 +0200 Subject: [PATCH 4/7] update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9daa608..321daa2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ padelclub_backend/settings_local.py myenv/ shared/config_local.py +logs/ # Byte-compiled / optimized / DLL files __pycache__/ From e1e2fb08ef1fab59652d8b75013681e799277db6 Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 22 Sep 2025 16:08:20 +0200 Subject: [PATCH 5/7] rename Campaign into ProspectGroup --- biz/admin.py | 10 ++--- biz/filters.py | 14 +++---- .../0007_prospectgroup_delete_campaign.py | 37 +++++++++++++++++++ biz/models.py | 4 +- tournaments/admin.py | 2 +- 5 files changed, 52 insertions(+), 15 deletions(-) create mode 100644 biz/migrations/0007_prospectgroup_delete_campaign.py diff --git a/biz/admin.py b/biz/admin.py index c853ffb..8fd2989 100644 --- a/biz/admin.py +++ b/biz/admin.py @@ -12,9 +12,9 @@ import io import time import logging -from .models import Entity, Prospect, Activity, Status, ActivityType, EmailTemplate, DeclinationReason, Campaign +from .models import Entity, Prospect, Activity, Status, ActivityType, EmailTemplate, DeclinationReason, ProspectGroup from .forms import FileImportForm, EmailTemplateSelectionForm -from .filters import ContactAgainFilter, ProspectStatusFilter, StaffUserFilter, ProspectProfileFilter, ProspectDeclineReasonFilter, ProspectCampaignFilter +from .filters import ContactAgainFilter, ProspectStatusFilter, StaffUserFilter, ProspectProfileFilter, ProspectDeclineReasonFilter, ProspectGroupFilter from tournaments.models import CustomUser from tournaments.models.enums import UserOrigin @@ -118,7 +118,7 @@ class ProspectAdmin(SyncedObjectAdmin): ] list_display = ('first_name', 'last_name', 'entity_names', 'last_update_date', 'current_status', 'current_text', 'contact_again') - list_filter = (ContactAgainFilter, ProspectStatusFilter, ProspectDeclineReasonFilter, ProspectCampaignFilter, 'creation_date', StaffUserFilter, 'source', ProspectProfileFilter) + list_filter = (ContactAgainFilter, ProspectStatusFilter, ProspectDeclineReasonFilter, ProspectGroupFilter, 'creation_date', StaffUserFilter, 'source', ProspectProfileFilter) search_fields = ('first_name', 'last_name', 'email', 'entities__name') date_hierarchy = 'creation_date' change_list_template = "admin/biz/prospect/change_list.html" @@ -401,8 +401,8 @@ class ProspectAdmin(SyncedObjectAdmin): time.sleep(1) -@admin.register(Campaign) -class CampaignAdmin(SyncedObjectAdmin): +@admin.register(ProspectGroup) +class ProspectGroupAdmin(SyncedObjectAdmin): list_display = ('name', 'user_count') date_hierarchy = 'creation_date' diff --git a/biz/filters.py b/biz/filters.py index 6e10e8c..6a03c5a 100644 --- a/biz/filters.py +++ b/biz/filters.py @@ -7,7 +7,7 @@ from django.utils import timezone from dateutil.relativedelta import relativedelta -from .models import Activity, Prospect, Status, DeclinationReason, Campaign +from .models import Activity, Prospect, Status, DeclinationReason, ProspectGroup User = get_user_model() @@ -110,17 +110,17 @@ class ProspectDeclineReasonFilter(admin.SimpleListFilter): else: return queryset -class ProspectCampaignFilter(admin.SimpleListFilter): - title = 'Campaign' - parameter_name = 'campaign' +class ProspectGroupFilter(admin.SimpleListFilter): + title = 'ProspectGroup' + parameter_name = 'prospect_group' def lookups(self, request, model_admin): - campaigns = Campaign.objects.all().order_by('-creation_date') - return [(campaign.id, campaign.name) for campaign in campaigns] + prospect_groups = ProspectGroup.objects.all().order_by('-creation_date') + return [(group.id, group.name) for group in prospect_groups] def queryset(self, request, queryset): if self.value(): - return queryset.filter(campaigns__id=self.value()) + return queryset.filter(prospect_groups__id=self.value()) return queryset class ContactAgainFilter(admin.SimpleListFilter): diff --git a/biz/migrations/0007_prospectgroup_delete_campaign.py b/biz/migrations/0007_prospectgroup_delete_campaign.py new file mode 100644 index 0000000..4603934 --- /dev/null +++ b/biz/migrations/0007_prospectgroup_delete_campaign.py @@ -0,0 +1,37 @@ +# Generated by Django 5.1 on 2025-09-22 14:08 + +import django.db.models.deletion +import django.utils.timezone +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('biz', '0006_alter_campaign_id'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ProspectGroup', + fields=[ + ('creation_date', models.DateTimeField(default=django.utils.timezone.now, editable=False)), + ('last_update', models.DateTimeField(default=django.utils.timezone.now)), + ('data_access_ids', models.JSONField(default=list)), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(blank=True, max_length=200, null=True)), + ('last_updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('prospects', models.ManyToManyField(blank=True, related_name='prospect_groups', to='biz.prospect')), + ('related_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.DeleteModel( + name='Campaign', + ), + ] diff --git a/biz/models.py b/biz/models.py index e271b18..cc3eb0f 100644 --- a/biz/models.py +++ b/biz/models.py @@ -171,10 +171,10 @@ class EmailTemplate(BaseModel): def delete_dependencies(self): pass -class Campaign(BaseModel): +class ProspectGroup(BaseModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True) name = models.CharField(max_length=200, null=True, blank=True) - prospects = models.ManyToManyField(Prospect, blank=True, related_name='campaigns') + prospects = models.ManyToManyField(Prospect, blank=True, related_name='prospect_groups') def user_count(self): return self.prospects.count() diff --git a/tournaments/admin.py b/tournaments/admin.py index 49d013f..c742a72 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -9,7 +9,7 @@ from django.shortcuts import render from django.db.models import Avg from datetime import timedelta, datetime -from biz.models import Prospect, Campaign +from biz.models import Prospect, ProspectGroup from .models import Club, TeamScore, Tournament, CustomUser, Event, Round, GroupStage, Match, TeamRegistration, PlayerRegistration, Purchase, Court, DateInterval, FailedApiCall, Log, DeviceToken, DrawLog, UnregisteredTeam, UnregisteredPlayer, Image from .forms import CustomUserCreationForm, CustomUserChangeForm From 3af02f98a71163ef4e0ca8000b69a8443af79c9c Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 22 Sep 2025 16:10:35 +0200 Subject: [PATCH 6/7] remove id change --- biz/migrations/0006_alter_campaign_id.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/biz/migrations/0006_alter_campaign_id.py b/biz/migrations/0006_alter_campaign_id.py index 9f829d8..46bdb24 100644 --- a/biz/migrations/0006_alter_campaign_id.py +++ b/biz/migrations/0006_alter_campaign_id.py @@ -11,9 +11,9 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AlterField( - model_name='campaign', - name='id', - field=models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False), - ), + # migrations.AlterField( + # model_name='campaign', + # name='id', + # field=models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False), + # ), ] From b3a20f69f4f43084409962eaf13dfcd081ef6565 Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 22 Sep 2025 16:12:22 +0200 Subject: [PATCH 7/7] fix issue --- tournaments/admin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tournaments/admin.py b/tournaments/admin.py index c742a72..de9596a 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -27,7 +27,7 @@ class CustomUserAdmin(UserAdmin): model = CustomUser search_fields = ['username', 'email', 'phone', 'first_name', 'last_name', 'licence_id'] filter_horizontal = ('clubs',) - actions = ['convert_to_prospect', 'create_campaign'] + actions = ['convert_to_prospect', 'create_group'] list_display = ['email', 'first_name', 'last_name', 'username', 'date_joined', 'latest_event_club_name', 'is_active', 'event_count', 'origin', 'registration_payment_mode', 'licence_id'] list_filter = ['is_active', 'origin', UserWithEventsFilter, UserWithPurchasesFilter, UserWithProspectFilter] @@ -59,7 +59,7 @@ class CustomUserAdmin(UserAdmin): obj.last_update = timezone.now() super().save_model(request, obj, form, change) - def create_campaign(self, request, queryset): + def create_group(self, request, queryset): prospects = [] source_value = f"auto_created_{datetime.now().strftime('%Y-%m-%d_%H:%M')}" for user in queryset: @@ -76,13 +76,13 @@ class CustomUserAdmin(UserAdmin): source=source_value ) prospects.append(prospect) - campaign = Campaign.objects.create( + prospect_group = ProspectGroup.objects.create( name=f"{datetime.now().strftime('%Y-%m-%d_%H:%M')}", ) - campaign.prospects.add(*prospects) - messages.success(request, f'Created campaign {campaign.name} with {queryset.count()} prospects') + prospect_group.prospects.add(*prospects) + messages.success(request, f'Created campaign {prospect_group.name} with {queryset.count()} prospects') - create_campaign.short_description = "Create campaign with selection" + create_group.short_description = "Create campaign with selection" def convert_to_prospect(self, request, queryset): created_count = 0