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
1 change: 1 addition & 0 deletions tickets/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
# Ticket related paths
path('ticket/<str:ticket_key>/transfer/', new_ticket.transfer_ticket, name='transfer_ticket'),
path('ticket/<str:ticket_key>/unassign/', new_ticket.unassign_ticket, name='unassign_ticket'),
path('ticket/<str:ticket_key>/unassign-check/', new_ticket.unassign_ticket_check, name='unassign_ticket_check'),
path('ticket/transfer-ticket/cancel-ticket-transfer', new_ticket.cancel_ticket_transfer,
name='cancel_ticket_transfer'),

Expand Down
54 changes: 48 additions & 6 deletions tickets/views/new_ticket.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
from django.db import transaction
from django.http import HttpResponseNotAllowed, HttpResponseForbidden, HttpResponse, HttpResponseBadRequest, \
JsonResponse
from django.shortcuts import redirect
from django.shortcuts import redirect, get_object_or_404
from django.urls import reverse
from django.utils import timezone
from django.db.models import Q
from allauth.account.models import EmailAddress

from events.models import GrupoMiembro
from tickets.models import NewTicket, NewTicketTransfer
from utils.email import send_mail

Expand Down Expand Up @@ -228,8 +230,8 @@ def assign_ticket(request, ticket_key):
if ticket.is_used:
return HttpResponseBadRequest('Cannot assign a used ticket')

if not ticket.event.transfer_period():
return HttpResponseBadRequest('Transfer period has ended')
# Vincular siempre permitido (incluso después de la fecha límite de transferencias),
# para quien tenga un bono desvinculado. Desvincular sí se bloquea después de esa fecha.

if NewTicket.objects.filter(holder=request.user, owner=request.user, event=ticket.event).exists():
return HttpResponseBadRequest('User already has a ticket')
Expand All @@ -250,14 +252,43 @@ def assign_ticket(request, ticket_key):
return redirect(reverse('my_ticket_event', kwargs={'event_slug': ticket.event.slug}))


@login_required()
def unassign_ticket_check(request, ticket_key):
"""GET: returns whether the user will be removed from early access / late checkout groups if they unlink."""
if request.method != 'GET':
return HttpResponseNotAllowed(['GET'])
ticket = get_object_or_404(NewTicket, key=ticket_key)
if not (ticket.holder == request.user and ticket.owner == request.user):
return JsonResponse({'error': 'No autorizado'}, status=403)
# Grupos del evento donde el usuario tiene ingreso anticipado o late checkout (y no es el líder)
memberships = GrupoMiembro.objects.filter(
user=request.user,
grupo__event=ticket.event
).exclude(
grupo__lider=request.user
).filter(
Q(ingreso_anticipado=True) | Q(late_checkout=True)
).select_related('grupo')
groups = [
{
'nombre': m.grupo.nombre,
'ingreso_anticipado': m.ingreso_anticipado,
'late_checkout': m.late_checkout,
}
for m in memberships
]
return JsonResponse({
'has_early_access_or_late_checkout': len(groups) > 0,
'groups': groups,
})


@login_required()
def unassign_ticket(request, ticket_key):
if request.method != 'POST':
return HttpResponseNotAllowed(['POST'])

ticket = NewTicket.objects.get(key=ticket_key)
if ticket is None:
return HttpResponseBadRequest()
ticket = get_object_or_404(NewTicket, key=ticket_key)

if not (ticket.holder == request.user and ticket.owner == request.user):
return HttpResponseForbidden()
Expand All @@ -271,6 +302,17 @@ def unassign_ticket(request, ticket_key):
if ticket.event.transfers_enabled_until < timezone.now():
return HttpResponseBadRequest('')

# Remover al usuario de grupos de este evento donde tiene ingreso anticipado o late checkout
# (no se toca al líder del grupo)
GrupoMiembro.objects.filter(
user=request.user,
grupo__event=ticket.event
).exclude(
grupo__lider=request.user
).filter(
Q(ingreso_anticipado=True) | Q(late_checkout=True)
).delete()

ticket.volunteer_ranger = None
ticket.volunteer_transmutator = None
ticket.volunteer_umpalumpa = None
Expand Down
5 changes: 2 additions & 3 deletions user_profile/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,17 +207,16 @@ class Meta:


class ProfileUpdateForm(forms.ModelForm):
"""Formulario para actualizar perfil"""
"""Formulario para actualizar perfil (nombre, documento). Teléfono se actualiza en la sección separada con verificación SMS."""
first_name = forms.CharField(max_length=30, widget=forms.TextInput(attrs={'class': 'form-control'}))
last_name = forms.CharField(max_length=30, widget=forms.TextInput(attrs={'class': 'form-control'}))

class Meta:
model = Profile
fields = ['document_type', 'document_number', 'phone']
fields = ['document_type', 'document_number']
widgets = {
'document_type': forms.Select(attrs={'class': 'form-select'}),
'document_number': forms.TextInput(attrs={'class': 'form-control'}),
'phone': forms.TextInput(attrs={'class': 'form-control'}),
}

def __init__(self, *args, **kwargs):
Expand Down
121 changes: 96 additions & 25 deletions user_profile/templates/mi_fuego/my_tickets/my_ticket.html
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,28 @@ <h3>No tenés bonos para este evento</h3>
{% endfor %}
{% endif %}

<div class="modal fade" id="unassignWarningModal" data-bs-backdrop="static" tabindex="-1" aria-labelledby="unassignWarningModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title text-warning" id="unassignWarningModalLabel"><i class="fas fa-exclamation-triangle me-2"></i>Aviso: grupos con ingreso anticipado o late checkout</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal-body">
<p class="mb-2">Estás en los siguientes grupos para este evento:</p>
<ul id="unassignWarningGroupsList" class="mb-3"></ul>
<div class="alert alert-warning mb-0">
<strong>Si desvinculás el bono</strong>, vas a ser removido de esas listas. Si más adelante volvés a vincular el bono, tendrás que pedir que te agreguen nuevamente.
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-danger" id="unassignConfirmBtn"><i class="fas fa-unlink me-1"></i> Desvincular el bono igualmente</button>
</div>
</div>
</div>
</div>

<div class="modal fade" id="transferModal" data-bs-backdrop="static" tabindex="-1" aria-labelledby="transferModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
Expand Down Expand Up @@ -777,38 +799,87 @@ <h5 class="modal-title" id="transferModalLabel">Transferir Bono</h5>
currentTicketKey = ticketId;
}

let pendingUnassignTicketKey = null;

function doUnassignTicket(ticketKey) {
fetch(`/ticket/${ticketKey}/unassign/`, {
method: 'POST',
headers: {
'X-CSRFToken': '{{ csrf_token }}',
'Content-Type': 'application/json',
},
})
.then(response => {
if (response.ok) {
window.location.reload();
} else {
alert('Error al desvincular el bono');
}
})
.catch(error => {
console.error('Error:', error);
alert('Error al desvincular el bono');
});
}

// Make function globally available
window.confirmOwnTicketTransfer = function(ticketKey) {
if (confirm('Este bono está a tu nombre. Desvinculalo para poder transferirselo a otra persona. Si te arrepientes, podés volver a vincularlo.')) {
// Unassign the ticket and refresh the page
fetch(`/ticket/${ticketKey}/unassign/`, {
method: 'POST',
headers: {
'X-CSRFToken': '{{ csrf_token }}',
'Content-Type': 'application/json',
},
})
.then(response => {
if (response.ok) {
// Refresh the page to show updated state
window.location.reload();
} else {
alert('Error al desvincular el bono');
window.confirmOwnTicketTransfer = async function(ticketKey) {
let hasGroupsWarning = false;
let groups = [];
try {
const checkRes = await fetch(`/ticket/${ticketKey}/unassign-check/`, { method: 'GET', headers: { 'Accept': 'application/json' } });
if (checkRes.ok) {
const data = await checkRes.json();
if (data.has_early_access_or_late_checkout && data.groups && data.groups.length > 0) {
hasGroupsWarning = true;
groups = data.groups;
}
})
.catch(error => {
console.error('Error:', error);
alert('Error al desvincular el bono');
});
}
} catch (e) {
console.warn('unassign-check failed', e);
}
if (hasGroupsWarning && groups.length > 0) {
const listEl = document.getElementById('unassignWarningGroupsList');
listEl.innerHTML = groups.map(g => {
const labels = [];
if (g.ingreso_anticipado) labels.push('ingreso anticipado');
if (g.late_checkout) labels.push('late checkout');
return `<li><strong>${escapeHtml(g.nombre)}</strong> (${labels.join(', ')})</li>`;
}).join('');
pendingUnassignTicketKey = ticketKey;
const modal = new bootstrap.Modal(document.getElementById('unassignWarningModal'));
modal.show();
} else {
const message = 'Este bono está a tu nombre. Desvinculalo para poder transferirselo a otra persona. Si te arrepientes, podés volver a vincularlo.';
if (confirm(message)) {
doUnassignTicket(ticketKey);
}
}
}

document.getElementById('unassignConfirmBtn').addEventListener('click', function() {
if (pendingUnassignTicketKey) {
const key = pendingUnassignTicketKey;
pendingUnassignTicketKey = null;
bootstrap.Modal.getInstance(document.getElementById('unassignWarningModal')).hide();
doUnassignTicket(key);
}
});

document.getElementById('unassignWarningModal').addEventListener('hidden.bs.modal', function() {
pendingUnassignTicketKey = null;
});

function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

document.querySelectorAll('.unassign-ticket').forEach(button => {
button.addEventListener('click', async function () {
button.addEventListener('click', function () {
const ticketKey = this.dataset.ticketKey;
if (confirm('Si no vas a ir, desasigna tu bono para poder transferirselo a otra persona. ¿Estás seguro que no vas a ir?')) {
window.location.href = `/ticket/${ticketKey}/unassign/`;
}
if (ticketKey) confirmOwnTicketTransfer(ticketKey);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ <h3>No tenés bonos adicionales</h3>
data-bs-toggle="modal"
data-bs-target="#transferModal"
onclick="setTransferTicketId('{{ ticket.key }}')">Transferir</button>
{% if not my_ticket %}
<a class="btn btn-sm btn-success text-white text-decoration-none"
href="{% url 'assign_ticket' ticket.key %}">Asignarmelo</a>
{% endif %}
{% endif %}
{% if not my_ticket %}
<a class="btn btn-sm btn-success text-white text-decoration-none"
href="{% url 'assign_ticket' ticket.key %}">Asignarmelo</a>
{% endif %}
{% endif %}
</div>
Expand Down
2 changes: 2 additions & 0 deletions user_profile/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,8 @@ def profile_view(request):
profile_form.save()
messages.success(request, 'Tu perfil ha sido actualizado exitosamente.')
return redirect('profile')
else:
messages.error(request, 'Revisá los datos e intentá de nuevo.')

# Handle password change
elif 'change_password' in request.POST:
Expand Down