From 8132826866fb9b4d6f4fbe58d7cf7845f8438853 Mon Sep 17 00:00:00 2001 From: Laurent Date: Wed, 24 Sep 2025 16:27:30 +0200 Subject: [PATCH 1/5] renamed agents into supervisors and added organizers who can create tournaments --- api/urls.py | 2 +- api/views.py | 8 +++--- tournaments/admin.py | 4 +-- ..._agents_customuser_supervisors_and_more.py | 28 +++++++++++++++++++ .../migrations/0139_customuser_organizers.py | 19 +++++++++++++ tournaments/models/custom_user.py | 5 ++-- 6 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 tournaments/migrations/0138_remove_customuser_agents_customuser_supervisors_and_more.py create mode 100644 tournaments/migrations/0139_customuser_organizers.py diff --git a/api/urls.py b/api/urls.py index 38c8381..5e9d5ff 100644 --- a/api/urls.py +++ b/api/urls.py @@ -8,7 +8,7 @@ from authentication.views import CustomAuthToken, Logout, ChangePasswordView router = routers.DefaultRouter() router.register(r'users', views.UserViewSet) -router.register(r'user-agents', views.ShortUserViewSet) +router.register(r'user-supervisors', views.SupervisorViewSet) router.register(r'clubs', views.ClubViewSet) router.register(r'tournaments', views.TournamentViewSet) router.register(r'tournament-summaries', views.TournamentSummaryViewSet) diff --git a/api/views.py b/api/views.py index e354117..439a86e 100644 --- a/api/views.py +++ b/api/views.py @@ -342,13 +342,13 @@ class UnregisteredPlayerViewSet(SoftDeleteViewSet): return self.queryset.filter(unregistered_team__tournament__event__creator=self.request.user) return [] -class ShortUserViewSet(viewsets.ModelViewSet): +class SupervisorViewSet(viewsets.ModelViewSet): queryset = CustomUser.objects.all() serializer_class = ShortUserSerializer permission_classes = [] # Users are public whereas the other requests are only for logged users def get_queryset(self): - return self.request.user.agents + return self.request.user.supervisors class ImageViewSet(viewsets.ModelViewSet): """ @@ -617,8 +617,8 @@ def validate_stripe_account(request): @permission_classes([IsAuthenticated]) def is_granted_unlimited_access(request): can_create = False - if request.user and request.user.is_anonymous == False and request.user.owners: - for owner in request.user.owners.all(): + if request.user and request.user.is_anonymous == False and request.user.organising_for: + for owner in request.user.organising_for.all(): purchases = Purchase.objects.filter(user=owner,product_id='app.padelclub.tournament.subscription.unlimited') for purchase in purchases: if purchase.is_active(): diff --git a/tournaments/admin.py b/tournaments/admin.py index 2e9a08b..f89f97a 100644 --- a/tournaments/admin.py +++ b/tournaments/admin.py @@ -32,7 +32,7 @@ class CustomUserAdmin(UserAdmin): 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] ordering = ['-date_joined'] - raw_id_fields = ['agents'] + raw_id_fields = ['supervisors', 'organizers'] fieldsets = [ (None, {'fields': ['id', 'username', 'email', 'password', 'first_name', 'last_name', 'is_active']}), ('Permissions', {'fields': ['is_staff', 'is_superuser', 'groups', 'user_permissions']}), @@ -41,7 +41,7 @@ class CustomUserAdmin(UserAdmin): 'summons_message_body', 'summons_message_signature', 'summons_available_payment_methods', 'summons_display_format', 'summons_display_entry_fee', 'summons_use_full_custom_message', 'match_formats_default_duration', 'bracket_match_format_preference', 'group_stage_match_format_preference', - 'loser_bracket_match_format_preference', 'device_id', 'loser_bracket_mode', 'origin', 'agents', 'should_synchronize', 'can_synchronize' + 'loser_bracket_match_format_preference', 'device_id', 'loser_bracket_mode', 'origin', 'supervisors', 'organizers', 'should_synchronize', 'can_synchronize' ]}), ] diff --git a/tournaments/migrations/0138_remove_customuser_agents_customuser_supervisors_and_more.py b/tournaments/migrations/0138_remove_customuser_agents_customuser_supervisors_and_more.py new file mode 100644 index 0000000..f6dc2c8 --- /dev/null +++ b/tournaments/migrations/0138_remove_customuser_agents_customuser_supervisors_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1 on 2025-09-24 14:19 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0137_playerregistration_is_anonymous_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='customuser', + name='agents', + ), + migrations.AddField( + model_name='customuser', + name='supervisors', + field=models.ManyToManyField(blank=True, related_name='supervising_for', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='tournament', + name='animation_type', + field=models.IntegerField(choices=[(0, 'Tournoi'), (1, 'Mêlée'), (2, 'Classement'), (3, 'Consolation'), (4, 'Custom')], default=0), + ), + ] diff --git a/tournaments/migrations/0139_customuser_organizers.py b/tournaments/migrations/0139_customuser_organizers.py new file mode 100644 index 0000000..7c834da --- /dev/null +++ b/tournaments/migrations/0139_customuser_organizers.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1 on 2025-09-24 14:20 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0138_remove_customuser_agents_customuser_supervisors_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='customuser', + name='organizers', + field=models.ManyToManyField(blank=True, related_name='organising_for', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/tournaments/models/custom_user.py b/tournaments/models/custom_user.py index 393f853..16ea447 100644 --- a/tournaments/models/custom_user.py +++ b/tournaments/models/custom_user.py @@ -36,7 +36,8 @@ class CustomUser(AbstractUser): loser_bracket_match_format_preference = models.IntegerField(default=enums.FederalMatchCategory.NINE_GAMES, choices=enums.FederalMatchCategory.choices, null=True, blank=True) device_id = models.CharField(max_length=50, null=True, blank=True) - agents = models.ManyToManyField('CustomUser', blank=True, related_name='owners') + supervisors = models.ManyToManyField('CustomUser', blank=True, related_name='supervising_for') + organizers = models.ManyToManyField('CustomUser', blank=True, related_name='organising_for') loser_bracket_mode = models.IntegerField(default=0) origin = models.IntegerField(default=enums.UserOrigin.ADMIN, choices=enums.UserOrigin.choices, null=True, blank=True) @@ -64,7 +65,7 @@ class CustomUser(AbstractUser): 'summons_display_format', 'summons_display_entry_fee', 'summons_use_full_custom_message', 'match_formats_default_duration', 'bracket_match_format_preference', 'group_stage_match_format_preference', 'loser_bracket_match_format_preference', 'device_id', 'loser_bracket_mode', - 'origin', 'agents', 'should_synchronize', 'user_role', 'registration_payment_mode', + 'origin', 'supervisors', 'organizers', 'should_synchronize', 'user_role', 'registration_payment_mode', 'umpire_custom_mail', 'umpire_custom_contact', 'umpire_custom_phone', 'hide_umpire_mail', 'hide_umpire_phone', 'disable_ranking_federal_ruling'] From 03f860cf48b940410b2fc97ee6fc19d02db6dc31 Mon Sep 17 00:00:00 2001 From: Laurent Date: Wed, 24 Sep 2025 16:52:21 +0200 Subject: [PATCH 2/5] improve mail sending error management --- biz/admin.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/biz/admin.py b/biz/admin.py index 3c4de33..789eece 100644 --- a/biz/admin.py +++ b/biz/admin.py @@ -361,9 +361,12 @@ class ProspectAdmin(SyncedObjectAdmin): if form.is_valid(): email_template = form.cleaned_data['email_template'] - self.process_selected_items_with_template(request, queryset, email_template) + sent_count, failed_count = 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) + if failed_count > 0: + self.message_user(request, f"Email sent to {sent_count} prospects, {failed_count} failed using the '{email_template.name}' template.", messages.WARNING) + else: + self.message_user(request, f"Email sent to {sent_count} prospects using the '{email_template.name}' template.", messages.SUCCESS) return HttpResponseRedirect(request.get_full_path()) else: form = EmailTemplateSelectionForm() @@ -398,9 +401,15 @@ class ProspectAdmin(SyncedObjectAdmin): sent_count += 1 except Exception as e: error_emails.append(prospect.email) + logger.error(f'Failed to send email to {prospect.email}: {str(e)}') time.sleep(1) + if error_emails: + logger.error(f'Failed to send emails to: {error_emails}') + + return sent_count, len(error_emails) + @admin.register(ProspectGroup) class ProspectGroupAdmin(SyncedObjectAdmin): list_display = ('name', 'user_count') From 9d71efb51a588cff6c9184b47523ab19a246e694 Mon Sep 17 00:00:00 2001 From: Laurent Date: Wed, 24 Sep 2025 17:01:45 +0200 Subject: [PATCH 3/5] adds an activity when sending an email --- biz/admin.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/biz/admin.py b/biz/admin.py index 789eece..77ad6f1 100644 --- a/biz/admin.py +++ b/biz/admin.py @@ -399,6 +399,13 @@ class ProspectAdmin(SyncedObjectAdmin): fail_silently=False, ) sent_count += 1 + + activity = Activity.objects.create( + type=ActivityType.MAIL, + status=Status.CONTACTED, + description=f"Email sent: {email_template.subject}" + ) + activity.prospects.add(prospect) except Exception as e: error_emails.append(prospect.email) logger.error(f'Failed to send email to {prospect.email}: {str(e)}') From e0047fbdc307013ae7c87c936ca3b4d18a8919b0 Mon Sep 17 00:00:00 2001 From: Laurent Date: Wed, 24 Sep 2025 17:36:17 +0200 Subject: [PATCH 4/5] ease search for prospects --- biz/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/biz/admin.py b/biz/admin.py index 77ad6f1..3892b06 100644 --- a/biz/admin.py +++ b/biz/admin.py @@ -119,7 +119,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, ProspectGroupFilter, PhoneFilter, 'creation_date', StaffUserFilter, 'source', ProspectProfileFilter) - search_fields = ('first_name', 'last_name', 'email', 'entities__name') + search_fields = ('first_name', 'last_name', 'email') date_hierarchy = 'creation_date' change_list_template = "admin/biz/prospect/change_list.html" ordering = ['-last_update'] From cf831be3c6768b85a1b12fcb69f924be67e57b70 Mon Sep 17 00:00:00 2001 From: Laurent Date: Wed, 24 Sep 2025 21:26:49 +0200 Subject: [PATCH 5/5] remove reset button --- biz/templates/admin/biz/prospect/change_list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/biz/templates/admin/biz/prospect/change_list.html b/biz/templates/admin/biz/prospect/change_list.html index ff9f26a..cdbb5d8 100644 --- a/biz/templates/admin/biz/prospect/change_list.html +++ b/biz/templates/admin/biz/prospect/change_list.html @@ -5,6 +5,6 @@
  • Import Import App Users - Reset +
  • {% endblock %}