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/biz/admin.py b/biz/admin.py
index 3c4de33..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']
@@ -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()
@@ -396,11 +399,24 @@ 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)}')
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')
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 %}
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']