Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions deprepagos/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from tickets.admin import admin_caja_view, email_has_account, admin_caja_order_view, admin_direct_tickets_view, \
admin_direct_tickets_buyer_view, admin_direct_tickets_congrats_view
from user_profile.admin_sede import admin_sede_home_view
from user_profile.admin_sede_members import admin_sede_members_view, admin_sede_multiple_active_view
from user_profile.admin_sede_matches import (
admin_sede_matches_assign,
admin_sede_matches_user_search,
Expand All @@ -22,6 +24,9 @@
path('admin/direct_tickets/buyer/', admin_direct_tickets_buyer_view, name='admin_direct_tickets_buyer_view'),
path('admin/direct_tickets/congrats/<int:new_order_id>/', admin_direct_tickets_congrats_view,
name='admin_direct_tickets_congrats_view'),
path('admin/sede/', admin_sede_home_view, name='admin_sede_home_view'),
path('admin/sede/members/', admin_sede_members_view, name='admin_sede_members_view'),
path('admin/sede/multiple-active/', admin_sede_multiple_active_view, name='admin_sede_multiple_active_view'),
path('admin/sede/subscriptions/', admin_sede_subscriptions_view, name='admin_sede_subscriptions_view'),
path('admin/sede/matches/', admin_sede_matches_view, name='admin_sede_matches_view'),
path('admin/sede/matches/users/search/', admin_sede_matches_user_search, name='admin_sede_matches_user_search'),
Expand Down
5 changes: 1 addition & 4 deletions tickets/templates/admin/base_site.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@ <h1 id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:_('D
{% endif %}
{% if user.is_staff %}
<li>
<a href="{% url 'admin_sede_subscriptions_view' %}">LA SEDE — SUBSCRIPTIONS</a>
</li>
<li>
<a href="{% url 'admin_sede_matches_view' %}">LA SEDE — MATCH AUDIT</a>
<a href="{% url 'admin_sede_home_view' %}">LA SEDE</a>
</li>
{% endif %}

Expand Down
8 changes: 2 additions & 6 deletions user_profile/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ class ProfileInline(admin.StackedInline):
can_delete = False
verbose_name_plural = 'profile'
readonly_fields = (
'sede_subscription_id', 'sede_subscription_status', 'sede_payment_method',
'sede_last_payment_date', 'sede_last_payment_amount', 'sede_next_payment_date',
'sede_member_since', 'sede_synced_at', 'sede_subscriptions_summary',
'sede_subscriptions_summary',
)
fieldsets = (
(None, {
Expand All @@ -26,9 +24,7 @@ class ProfileInline(admin.StackedInline):
}),
('La Sede', {
'fields': (
'miembro_sede', 'sede_subscription_id', 'sede_subscription_status',
'sede_payment_method', 'sede_last_payment_date', 'sede_last_payment_amount',
'sede_next_payment_date', 'sede_member_since', 'sede_synced_at', 'sede_subscriptions_summary',
'sede_subscriptions_summary',
),
}),
)
Expand Down
9 changes: 9 additions & 0 deletions user_profile/admin_sede.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.contrib.admin.views.decorators import staff_member_required
from django.shortcuts import redirect
from django.views.decorators.http import require_http_methods


@staff_member_required
@require_http_methods(['GET'])
def admin_sede_home_view(request):
return redirect('admin_sede_members_view')
3 changes: 3 additions & 0 deletions user_profile/admin_sede_matches.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def admin_sede_matches_view(request):
{
'title': 'La Sede — Unmatched Active Subscriptions',
'unmatched_rows': unmatched_rows,
'sede_section': 'matches',
},
)

Expand Down Expand Up @@ -93,6 +94,8 @@ def admin_sede_matches_assign(request):
unmatched = SedeUnmatchedSubscription.objects.filter(id=unmatched_id).first()
if not unmatched:
return JsonResponse({'ok': False, 'error': 'Unmatched subscription not found'}, status=404)
if (unmatched.status or '').lower() != 'authorized':
return JsonResponse({'ok': False, 'error': 'Only authorized subscriptions can be matched manually'}, status=400)

user = User.objects.select_related('profile').filter(id=user_id, profile__isnull=False).first()
if not user:
Expand Down
74 changes: 74 additions & 0 deletions user_profile/admin_sede_members.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from django.contrib.admin.views.decorators import staff_member_required
from django.db.models import Count, Q
from django.shortcuts import render
from django.views.decorators.http import require_http_methods

from user_profile.models import Profile
from user_profile.services.sede_mercadopago import format_payment_method


@staff_member_required
@require_http_methods(['GET'])
def admin_sede_members_view(request):
profiles = (
Profile.objects.filter(sede_subscriptions__isnull=False)
.select_related('user')
.prefetch_related('sede_subscriptions')
.distinct()
.order_by('user__last_name', 'user__first_name', 'user__email')
)

rows = []
for profile in profiles:
subs = list(profile.sede_subscriptions.all().order_by('-is_active', '-last_payment_date', '-synced_at'))
rows.append({
'profile': profile,
'subscriptions': subs,
'active_count': sum(1 for sub in subs if sub.is_active),
})

return render(
request,
'admin/admin_sede_members.html',
{
'title': 'La Sede — Matched Members',
'rows': rows,
'sede_section': 'members',
'format_payment_method': format_payment_method,
},
)


@staff_member_required
@require_http_methods(['GET'])
def admin_sede_multiple_active_view(request):
profiles = (
Profile.objects.filter(sede_subscriptions__isnull=False)
.annotate(
active_count=Count('sede_subscriptions', filter=Q(sede_subscriptions__is_active=True))
)
.filter(active_count__gt=1)
.select_related('user')
.prefetch_related('sede_subscriptions')
.order_by('-active_count', 'user__last_name', 'user__first_name', 'user__email')
)

rows = []
for profile in profiles:
subs = list(profile.sede_subscriptions.all().order_by('-is_active', '-last_payment_date', '-synced_at'))
rows.append({
'profile': profile,
'subscriptions': subs,
'active_count': profile.active_count,
})

return render(
request,
'admin/admin_sede_multiple_active.html',
{
'title': 'La Sede — Multiple Active Subscriptions',
'rows': rows,
'sede_section': 'multiple_active',
'format_payment_method': format_payment_method,
},
)
1 change: 1 addition & 0 deletions user_profile/admin_sede_subscriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,6 @@ def admin_sede_subscriptions_view(request):
{
'title': 'La Sede — Subscription Plans',
'plans': plans,
'sede_section': 'subscriptions',
},
)
57 changes: 57 additions & 0 deletions user_profile/migrations/0008_sede_subscription_redesign.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Generated by Django 4.2.15 on 2026-05-24 16:00

from django.db import migrations


def clear_sede_tables(apps, schema_editor):
SedeSubscription = apps.get_model('user_profile', 'SedeSubscription')
SedeUnmatchedSubscription = apps.get_model('user_profile', 'SedeUnmatchedSubscription')
SedeSubscription.objects.all().delete()
SedeUnmatchedSubscription.objects.all().delete()


class Migration(migrations.Migration):

dependencies = [
('user_profile', '0007_sedesubscriptionplan'),
]

operations = [
migrations.RemoveField(
model_name='profile',
name='miembro_sede',
),
migrations.RemoveField(
model_name='profile',
name='sede_last_payment_amount',
),
migrations.RemoveField(
model_name='profile',
name='sede_last_payment_date',
),
migrations.RemoveField(
model_name='profile',
name='sede_member_since',
),
migrations.RemoveField(
model_name='profile',
name='sede_next_payment_date',
),
migrations.RemoveField(
model_name='profile',
name='sede_payment_method',
),
migrations.RemoveField(
model_name='profile',
name='sede_subscription_id',
),
migrations.RemoveField(
model_name='profile',
name='sede_subscription_status',
),
migrations.RemoveField(
model_name='profile',
name='sede_synced_at',
),
migrations.RunPython(clear_sede_tables, migrations.RunPython.noop),
]
64 changes: 55 additions & 9 deletions user_profile/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,61 @@ class Profile(BaseModel):
phone = models.CharField(max_length=15, validators=[RegexValidator(r'^\+?1?\d{9,15}$')])
profile_completion = models.CharField(max_length=15, choices=PROFILE_COMPLETION_CHOICES, default=NONE)

miembro_sede = models.BooleanField(default=False, verbose_name='Miembro de La Sede')
sede_subscription_id = models.CharField(max_length=64, blank=True, default='')
sede_subscription_status = models.CharField(max_length=32, blank=True, default='')
sede_payment_method = models.CharField(max_length=64, blank=True, default='')
sede_last_payment_date = models.DateTimeField(null=True, blank=True)
sede_last_payment_amount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
sede_next_payment_date = models.DateTimeField(null=True, blank=True)
sede_member_since = models.DateTimeField(null=True, blank=True)
sede_synced_at = models.DateTimeField(null=True, blank=True)
def _primary_sede_subscription(self):
if hasattr(self, '_cached_primary_sede_subscription'):
return self._cached_primary_sede_subscription
subs_qs = self.sede_subscriptions.all()
active = subs_qs.filter(is_active=True).order_by('-last_payment_date', '-synced_at').first()
if active:
self._cached_primary_sede_subscription = active
return active
fallback = subs_qs.order_by('-last_payment_date', '-synced_at').first()
self._cached_primary_sede_subscription = fallback
return fallback

@property
def miembro_sede(self):
return self.sede_subscriptions.filter(is_active=True).exists()

@property
def sede_subscription_id(self):
primary = self._primary_sede_subscription()
return primary.subscription_id if primary else ''

@property
def sede_subscription_status(self):
primary = self._primary_sede_subscription()
return primary.status if primary else ''

@property
def sede_payment_method(self):
primary = self._primary_sede_subscription()
return primary.payment_method if primary else ''

@property
def sede_last_payment_date(self):
primary = self._primary_sede_subscription()
return primary.last_payment_date if primary else None

@property
def sede_last_payment_amount(self):
primary = self._primary_sede_subscription()
return primary.last_payment_amount if primary else None

@property
def sede_next_payment_date(self):
primary = self._primary_sede_subscription()
return primary.next_payment_date if primary else None

@property
def sede_member_since(self):
primary = self._primary_sede_subscription()
return primary.member_since if primary else None

@property
def sede_synced_at(self):
primary = self._primary_sede_subscription()
return primary.synced_at if primary else None

@property
def sede_payment_method_label(self):
Expand Down
Loading
Loading