forked from CGM_Public/pretix_original
Gift cards: Improved support for cross-organizer acceptance (#3311)
Co-authored-by: Martin Gross <martin@pc-coholic.de>
This commit is contained in:
@@ -60,6 +60,8 @@ class NestedGiftCardSerializer(GiftCardSerializer):
|
||||
|
||||
|
||||
class ReusableMediaSerializer(I18nAwareModelSerializer):
|
||||
organizer = serializers.SlugRelatedField(slug_field='slug', read_only=True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -111,6 +113,7 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
||||
model = ReusableMedium
|
||||
fields = (
|
||||
'id',
|
||||
'organizer',
|
||||
'created',
|
||||
'updated',
|
||||
'type',
|
||||
|
||||
@@ -36,9 +36,9 @@ from pretix.api.serializers.settings import SettingsSerializer
|
||||
from pretix.base.auth import get_auth_backends
|
||||
from pretix.base.i18n import get_language_without_region
|
||||
from pretix.base.models import (
|
||||
Customer, Device, GiftCard, GiftCardTransaction, Membership,
|
||||
MembershipType, OrderPosition, Organizer, ReusableMedium, SeatingPlan,
|
||||
Team, TeamAPIToken, TeamInvite, User,
|
||||
Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction,
|
||||
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
|
||||
SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||
)
|
||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
@@ -183,8 +183,11 @@ class GiftCardSerializer(I18nAwareModelSerializer):
|
||||
qs = GiftCard.objects.filter(
|
||||
secret=s
|
||||
).filter(
|
||||
Q(issuer=self.context["organizer"]) | Q(
|
||||
issuer__gift_card_collector_acceptance__collector=self.context["organizer"])
|
||||
Q(issuer=self.context["organizer"]) |
|
||||
Q(issuer__in=GiftCardAcceptance.objects.filter(
|
||||
acceptor=self.context["organizer"],
|
||||
active=True,
|
||||
).values_list('issuer', flat=True))
|
||||
)
|
||||
if self.instance:
|
||||
qs = qs.exclude(pk=self.instance.pk)
|
||||
|
||||
@@ -39,7 +39,8 @@ from pretix.api.serializers.media import (
|
||||
)
|
||||
from pretix.base.media import MEDIA_TYPES
|
||||
from pretix.base.models import (
|
||||
Checkin, GiftCard, GiftCardTransaction, OrderPosition, ReusableMedium,
|
||||
Checkin, GiftCard, GiftCardAcceptance, GiftCardTransaction, OrderPosition,
|
||||
ReusableMedium,
|
||||
)
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
@@ -135,12 +136,28 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
|
||||
s = self.get_serializer(m)
|
||||
return Response({"result": s.data})
|
||||
except ReusableMedium.DoesNotExist:
|
||||
mt = MEDIA_TYPES.get(s.validated_data["type"])
|
||||
if mt:
|
||||
m = mt.handle_unknown(request.organizer, s.validated_data["identifier"], request.user, request.auth)
|
||||
if m:
|
||||
s = self.get_serializer(m)
|
||||
return Response({"result": s.data})
|
||||
try:
|
||||
with scopes_disabled():
|
||||
m = ReusableMedium.objects.get(
|
||||
organizer__in=GiftCardAcceptance.objects.filter(
|
||||
acceptor=request.organizer,
|
||||
active=True,
|
||||
reusable_media=True,
|
||||
).values_list('issuer', flat=True),
|
||||
type=s.validated_data["type"],
|
||||
identifier=s.validated_data["identifier"],
|
||||
)
|
||||
m.linked_orderposition = None # not relevant for cross-organizer
|
||||
m.customer = None # not relevant for cross-organizer
|
||||
s = self.get_serializer(m)
|
||||
return Response({"result": s.data})
|
||||
except ReusableMedium.DoesNotExist:
|
||||
mt = MEDIA_TYPES.get(s.validated_data["type"])
|
||||
if mt:
|
||||
m = mt.handle_unknown(request.organizer, s.validated_data["identifier"], request.user, request.auth)
|
||||
if m:
|
||||
s = self.get_serializer(m)
|
||||
return Response({"result": s.data})
|
||||
|
||||
return Response({"result": None})
|
||||
|
||||
|
||||
@@ -1147,7 +1147,7 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
def iterate_list(self, form_data):
|
||||
qs = GiftCardTransaction.objects.filter(
|
||||
card__issuer=self.organizer,
|
||||
).order_by('datetime').select_related('card', 'order', 'order__event')
|
||||
).order_by('datetime').select_related('card', 'order', 'order__event', 'acceptor')
|
||||
|
||||
if form_data.get('date_range'):
|
||||
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['date_range'], self.timezone)
|
||||
@@ -1163,6 +1163,7 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
_('Amount'),
|
||||
_('Currency'),
|
||||
_('Order'),
|
||||
_('Organizer'),
|
||||
]
|
||||
yield headers
|
||||
|
||||
@@ -1174,6 +1175,7 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
obj.value,
|
||||
obj.card.currency,
|
||||
obj.order.full_code if obj.order else None,
|
||||
str(obj.acceptor or ""),
|
||||
]
|
||||
yield row
|
||||
|
||||
|
||||
38
src/pretix/base/migrations/0242_auto_20230512_1008.py
Normal file
38
src/pretix/base/migrations/0242_auto_20230512_1008.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 3.2.18 on 2023-05-12 10:08
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0241_itemmetaproperties_required_values'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='giftcardacceptance',
|
||||
old_name='collector',
|
||||
new_name='acceptor',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='giftcardacceptance',
|
||||
name='active',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='giftcardacceptance',
|
||||
name='reusable_media',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='giftcardacceptance',
|
||||
name='issuer',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gift_card_acceptor_acceptance', to='pretixbase.organizer'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='giftcardacceptance',
|
||||
unique_together={('issuer', 'acceptor')},
|
||||
),
|
||||
]
|
||||
@@ -46,14 +46,19 @@ def gen_giftcard_secret(length=8):
|
||||
class GiftCardAcceptance(models.Model):
|
||||
issuer = models.ForeignKey(
|
||||
'Organizer',
|
||||
related_name='gift_card_collector_acceptance',
|
||||
related_name='gift_card_acceptor_acceptance',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
collector = models.ForeignKey(
|
||||
acceptor = models.ForeignKey(
|
||||
'Organizer',
|
||||
related_name='gift_card_issuer_acceptance',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
active = models.BooleanField(default=True)
|
||||
reusable_media = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
unique_together = (('issuer', 'acceptor'),)
|
||||
|
||||
|
||||
class GiftCard(LoggedModel):
|
||||
@@ -114,7 +119,7 @@ class GiftCard(LoggedModel):
|
||||
return self.transactions.aggregate(s=Sum('value'))['s'] or Decimal('0.00')
|
||||
|
||||
def accepted_by(self, organizer):
|
||||
return self.issuer == organizer or GiftCardAcceptance.objects.filter(issuer=self.issuer, collector=organizer).exists()
|
||||
return self.issuer == organizer or GiftCardAcceptance.objects.filter(issuer=self.issuer, acceptor=organizer, active=True).exists()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.secret:
|
||||
|
||||
@@ -40,7 +40,7 @@ from django.conf import settings
|
||||
from django.core.mail import get_connection
|
||||
from django.core.validators import MinLengthValidator, RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
@@ -157,17 +157,19 @@ class Organizer(LoggedModel):
|
||||
return self.cache.get_or_set(
|
||||
key='has_gift_cards',
|
||||
timeout=15,
|
||||
default=lambda: self.issued_gift_cards.exists() or self.gift_card_issuer_acceptance.exists()
|
||||
default=lambda: self.issued_gift_cards.exists() or self.gift_card_issuer_acceptance.filter(active=True).exists()
|
||||
)
|
||||
|
||||
@property
|
||||
def accepted_gift_cards(self):
|
||||
from .giftcards import GiftCard, GiftCardAcceptance
|
||||
|
||||
return GiftCard.objects.annotate(
|
||||
accepted=Exists(GiftCardAcceptance.objects.filter(issuer=OuterRef('issuer'), collector=self))
|
||||
).filter(
|
||||
Q(issuer=self) | Q(accepted=True)
|
||||
return GiftCard.objects.filter(
|
||||
Q(issuer=self) |
|
||||
Q(issuer__in=GiftCardAcceptance.objects.filter(
|
||||
acceptor=self,
|
||||
active=True,
|
||||
).values_list('issuer', flat=True))
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -65,8 +65,8 @@ from pretix.base.forms.questions import (
|
||||
)
|
||||
from pretix.base.forms.widgets import SplitDateTimePickerWidget
|
||||
from pretix.base.models import (
|
||||
Customer, Device, EventMetaProperty, Gate, GiftCard, Membership,
|
||||
MembershipType, OrderPosition, Organizer, ReusableMedium, Team,
|
||||
Customer, Device, EventMetaProperty, Gate, GiftCard, GiftCardAcceptance,
|
||||
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium, Team,
|
||||
)
|
||||
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
|
||||
from pretix.base.models.organizer import OrganizerFooterLink
|
||||
@@ -637,7 +637,11 @@ class GiftCardCreateForm(forms.ModelForm):
|
||||
if GiftCard.objects.filter(
|
||||
secret__iexact=s
|
||||
).filter(
|
||||
Q(issuer=self.organizer) | Q(issuer__gift_card_collector_acceptance__collector=self.organizer)
|
||||
Q(issuer=self.organizer) |
|
||||
Q(issuer__in=GiftCardAcceptance.objects.filter(
|
||||
acceptor=self.organizer,
|
||||
active=True,
|
||||
).values_list('issuer', flat=True))
|
||||
).exists():
|
||||
raise ValidationError(
|
||||
_('A gift card with the same secret already exists in your or an affiliated organizer account.')
|
||||
@@ -1026,3 +1030,32 @@ class SSOClientForm(I18nModelForm):
|
||||
else:
|
||||
del self.fields['client_id']
|
||||
del self.fields['regenerate_client_secret']
|
||||
|
||||
|
||||
class GiftCardAcceptanceInviteForm(forms.Form):
|
||||
acceptor = forms.CharField(
|
||||
label=_("Organizer short name"),
|
||||
required=True,
|
||||
)
|
||||
reusable_media = forms.BooleanField(
|
||||
label=_("Allow access to reusable media"),
|
||||
help_text=_("This is required if you want the other organizer to participate in a shared system with e.g. "
|
||||
"NFC payment chips. You should only use this option for organizers you trust, since (depending "
|
||||
"on the activated medium types) this will grant the other organizer access to cryptographic key "
|
||||
"material required to interact with the media type."),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.organizer = kwargs.pop('organizer')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean_acceptor(self):
|
||||
val = self.cleaned_data['acceptor']
|
||||
try:
|
||||
acceptor = Organizer.objects.exclude(pk=self.organizer.pk).get(slug=val)
|
||||
except Organizer.DoesNotExist:
|
||||
raise ValidationError(_('The selected organizer does not exist or cannot be invited.'))
|
||||
if self.organizer.gift_card_acceptor_acceptance.filter(acceptor=acceptor).exists():
|
||||
raise ValidationError(_('The selected organizer has already been invited.'))
|
||||
return acceptor
|
||||
|
||||
@@ -340,6 +340,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.organizer.export.schedule.failed': _('A scheduled export has failed: {reason}.'),
|
||||
'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'),
|
||||
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
|
||||
'pretix.giftcards.acceptance.acceptor.invited': _('A new gift card acceptor has been invited.'),
|
||||
'pretix.giftcards.acceptance.issuer.removed': _('A gift card issuer has been removed or declined.'),
|
||||
'pretix.giftcards.acceptance.issuer.accepted': _('A new gift card issuer has been accepted.'),
|
||||
'pretix.webhook.created': _('The webhook has been created.'),
|
||||
'pretix.webhook.changed': _('The webhook has been changed.'),
|
||||
'pretix.webhook.retries.expedited': _('The webhook call retry jobs have been manually expedited.'),
|
||||
|
||||
@@ -519,13 +519,32 @@ def get_organizer_navigation(request):
|
||||
})
|
||||
|
||||
if 'can_manage_gift_cards' in request.orgapermset:
|
||||
children = []
|
||||
children.append({
|
||||
'label': _('Gift cards'),
|
||||
'url': reverse('control:organizer.giftcards', kwargs={
|
||||
'organizer': request.organizer.slug
|
||||
}),
|
||||
'active': 'organizer.giftcard' in url.url_name and 'acceptance' not in url.url_name,
|
||||
'children': children,
|
||||
})
|
||||
if 'can_change_organizer_settings' in request.orgapermset:
|
||||
children.append(
|
||||
{
|
||||
'label': _('Acceptance'),
|
||||
'url': reverse('control:organizer.giftcards.acceptance', kwargs={
|
||||
'organizer': request.organizer.slug
|
||||
}),
|
||||
'active': 'organizer.giftcards.acceptance' in url.url_name,
|
||||
}
|
||||
)
|
||||
nav.append({
|
||||
'label': _('Gift cards'),
|
||||
'url': reverse('control:organizer.giftcards', kwargs={
|
||||
'organizer': request.organizer.slug
|
||||
}),
|
||||
'active': 'organizer.giftcard' in url.url_name,
|
||||
'icon': 'credit-card',
|
||||
'children': children,
|
||||
})
|
||||
|
||||
if request.organizer.settings.customer_accounts:
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
<a href="{% url "control:organizer.giftcard" organizer=gc.issuer.slug giftcard=gc.pk %}">
|
||||
{{ gc.secret }}
|
||||
</a>
|
||||
{% if gc.issuer != request.organizer %}
|
||||
<span class="text-muted">
|
||||
<br>
|
||||
<span class="fa fa-group"></span> {{ gc.issuer }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>{% trans "Issuer" %}</dt>
|
||||
<dd>{{ gc.issuer }}</dd>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load urlreplace %}
|
||||
{% load bootstrap3 %}
|
||||
{% load money %}
|
||||
{% block inner %}
|
||||
<h1>
|
||||
{% trans "Invite organizer" %}
|
||||
</h1>
|
||||
<form class="form-horizontal" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form layout="control" %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,150 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load urlreplace %}
|
||||
{% load bootstrap3 %}
|
||||
{% load money %}
|
||||
{% block inner %}
|
||||
<h1>
|
||||
{% trans "Gift cards acceptance" %}
|
||||
</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
This feature allows you to configure acceptance of gift cards across multiple organizer accounts.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<h2>
|
||||
{% trans "Other organizers you accept gift cards from" %}
|
||||
</h2>
|
||||
|
||||
{% if issuer_acceptance|length == 0 and not filter_form.filtered %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You are not accepting gift cards from other organizers yet. If you want to do so, the other
|
||||
organizer can add you to their list and afterwards, you can confirm this here.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% else %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Organizer" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Reusable media" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for gca in issuer_acceptance %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ gca.issuer.name }}<br><code>{{ gca.issuer.slug }}</code>
|
||||
</td>
|
||||
<td>
|
||||
{% if gca.active %}
|
||||
{% trans "active" %}
|
||||
{% else %}
|
||||
{% trans "invited" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if gca.reusable_media %}
|
||||
{% trans "active" %}
|
||||
{% else %}
|
||||
{% trans "disabled" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if gca.active %}
|
||||
<button class="btn btn-danger" name="delete_issuer" value="{{ gca.issuer.slug }}">
|
||||
{% trans "Remove" %}
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="btn btn-success" name="accept_issuer" value="{{ gca.issuer.slug }}">
|
||||
{% trans "Accept" %}
|
||||
</button>
|
||||
<button class="btn btn-danger" name="delete_issuer" value="{{ gca.issuer.slug }}">
|
||||
{% trans "Decline" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</form>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<h2>
|
||||
{% trans "Other organizers accepting gift cards from you" %}
|
||||
</h2>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You can invite other organizers to accept your gift cards. After you have done so, they need to go
|
||||
to the same page in their account and accept your invitation. Note that other organizers will be able
|
||||
to add money to gift cards as well that you will need to collect form them. It is your responsibility
|
||||
to handle the exchange of money to offset the transactions between the two organizers.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You can optionally control whether they can access your reusable media. This is required if you want
|
||||
them to participate in a shared system with e.g. NFC payment chips.
|
||||
{% endblocktrans %}
|
||||
{% blocktrans trimmed %}
|
||||
You should only use this option for organizers you trust, since (depending on the activated medium types)
|
||||
this will grant the other organizer access to cryptographic key material required to interact with
|
||||
the media type.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<a href="{% url "control:organizer.giftcards.acceptance.invite" organizer=request.organizer.slug %}" class="btn btn-default">
|
||||
{% trans "Invite new organizer" %}
|
||||
</a>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Organizer" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Reusable media" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for gca in acceptor_acceptance %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ gca.acceptor.name }}<br><code>{{ gca.acceptor.slug }}</code>
|
||||
</td>
|
||||
<td>
|
||||
{% if gca.active %}
|
||||
{% trans "active" %}
|
||||
{% else %}
|
||||
{% trans "invited" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if gca.reusable_media %}
|
||||
{% trans "active" %}
|
||||
{% else %}
|
||||
{% trans "disabled" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<button class="btn btn-danger" name="delete_acceptor" value="{{ gca.acceptor.slug }}">
|
||||
{% trans "Remove" %}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endblock %}
|
||||
@@ -99,44 +99,4 @@
|
||||
</div>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endif %}
|
||||
{% if not is_paginated or page_obj.number == 1 %}
|
||||
<form action="" method="post" class="form-inline">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Accepted gift cards of other organizers" %}</legend>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
If you have access to multiple organizer accounts, you can configure that ticket shops in
|
||||
this account will also accept gift codes issued through a different organizer account, and
|
||||
vice versa.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<ul>
|
||||
{% for gca in request.organizer.gift_card_issuer_acceptance.all %}
|
||||
<li>
|
||||
<strong>{{ gca.issuer }}</strong>
|
||||
<button type="submit" name="del" value="{{ gca.issuer.slug }}" class="btn btn-xs btn-danger">
|
||||
<span class="fa fa-trash"></span>
|
||||
</button>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li>
|
||||
<em>{% trans "You are currently not accepting gift cards from other organizers." %}</em>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if other_organizers %}
|
||||
<li>
|
||||
<select name="add" class="form-control input-sm">
|
||||
<option></option>
|
||||
{% for o in other_organizers %}
|
||||
<option value="{{ o.slug }}">{{ o }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn btn-primary btn-sm" type="submit"><span class="fa fa-plus"></span></button>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -176,6 +176,10 @@ urlpatterns = [
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/giftcard/(?P<giftcard>[^/]+)/$', organizer.GiftCardDetailView.as_view(), name='organizer.giftcard'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/giftcard/(?P<giftcard>[^/]+)/edit$', organizer.GiftCardUpdateView.as_view(),
|
||||
name='organizer.giftcard.edit'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/giftcards/acceptance$', organizer.GiftCardAcceptanceListView.as_view(),
|
||||
name='organizer.giftcards.acceptance'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/giftcards/acceptance/invite$', organizer.GiftCardAcceptanceInviteView.as_view(),
|
||||
name='organizer.giftcards.acceptance.invite'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/webhooks$', organizer.WebHookListView.as_view(), name='organizer.webhooks'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/webhook/add$', organizer.WebHookCreateView.as_view(),
|
||||
name='organizer.webhook.add'),
|
||||
|
||||
@@ -77,7 +77,7 @@ from pretix.base.models import (
|
||||
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
|
||||
from pretix.base.models.event import Event, EventMetaProperty, EventMetaValue
|
||||
from pretix.base.models.giftcards import (
|
||||
GiftCardTransaction, gen_giftcard_secret,
|
||||
GiftCardAcceptance, GiftCardTransaction, gen_giftcard_secret,
|
||||
)
|
||||
from pretix.base.models.orders import CancellationRequest
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
@@ -96,12 +96,12 @@ from pretix.control.forms.filter import (
|
||||
from pretix.control.forms.orders import ExporterForm
|
||||
from pretix.control.forms.organizer import (
|
||||
CustomerCreateForm, CustomerUpdateForm, DeviceBulkEditForm, DeviceForm,
|
||||
EventMetaPropertyForm, GateForm, GiftCardCreateForm, GiftCardUpdateForm,
|
||||
MailSettingsForm, MembershipTypeForm, MembershipUpdateForm,
|
||||
OrganizerDeleteForm, OrganizerFooterLinkFormset, OrganizerForm,
|
||||
OrganizerSettingsForm, OrganizerUpdateForm, ReusableMediumCreateForm,
|
||||
ReusableMediumUpdateForm, SSOClientForm, SSOProviderForm, TeamForm,
|
||||
WebHookForm,
|
||||
EventMetaPropertyForm, GateForm, GiftCardAcceptanceInviteForm,
|
||||
GiftCardCreateForm, GiftCardUpdateForm, MailSettingsForm,
|
||||
MembershipTypeForm, MembershipUpdateForm, OrganizerDeleteForm,
|
||||
OrganizerFooterLinkFormset, OrganizerForm, OrganizerSettingsForm,
|
||||
OrganizerUpdateForm, ReusableMediumCreateForm, ReusableMediumUpdateForm,
|
||||
SSOClientForm, SSOProviderForm, TeamForm, WebHookForm,
|
||||
)
|
||||
from pretix.control.forms.rrule import RRuleForm
|
||||
from pretix.control.logdisplay import OVERVIEW_BANLIST
|
||||
@@ -181,7 +181,8 @@ class OrganizerDetail(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
|
||||
return self.request.organizer
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.user.get_events_with_any_permission(self.request).select_related('organizer').prefetch_related(
|
||||
qs = self.request.user.get_events_with_any_permission(self.request).select_related(
|
||||
'organizer').prefetch_related(
|
||||
'organizer', '_settings_objects', 'organizer___settings_objects',
|
||||
'organizer__meta_properties',
|
||||
Prefetch(
|
||||
@@ -211,7 +212,8 @@ class OrganizerDetail(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['filter_form'] = self.filter_form
|
||||
ctx['meta_fields'] = [
|
||||
self.filter_form['meta_{}'.format(p.name)] for p in self.organizer.meta_properties.filter(filter_allowed=True)
|
||||
self.filter_form['meta_{}'.format(p.name)] for p in
|
||||
self.organizer.meta_properties.filter(filter_allowed=True)
|
||||
]
|
||||
return ctx
|
||||
|
||||
@@ -316,7 +318,8 @@ class MailSettingsPreview(OrganizerPermissionRequiredMixin, View):
|
||||
# get all supported placeholders with dummy values
|
||||
def placeholders(self, item):
|
||||
ctx = {}
|
||||
for p, s in MailSettingsForm(obj=self.request.organizer)._get_sample_context(MailSettingsForm.base_context[item]).items():
|
||||
for p, s in MailSettingsForm(obj=self.request.organizer)._get_sample_context(
|
||||
MailSettingsForm.base_context[item]).items():
|
||||
if s.strip().startswith('*'):
|
||||
ctx[p] = s
|
||||
else:
|
||||
@@ -341,7 +344,8 @@ class MailSettingsPreview(OrganizerPermissionRequiredMixin, View):
|
||||
if idx in self.supported_locale:
|
||||
with language(self.supported_locale[idx], self.request.organizer.settings.region):
|
||||
if k.startswith('mail_subject_'):
|
||||
msgs[self.supported_locale[idx]] = format_map(bleach.clean(v), self.placeholders(preview_item))
|
||||
msgs[self.supported_locale[idx]] = format_map(bleach.clean(v),
|
||||
self.placeholders(preview_item))
|
||||
else:
|
||||
msgs[self.supported_locale[idx]] = markdown_compile_email(
|
||||
format_map(v, self.placeholders(preview_item))
|
||||
@@ -395,7 +399,8 @@ class OrganizerDelete(AdministratorPermissionRequiredMixin, FormView):
|
||||
messages.success(self.request, _('The organizer has been deleted.'))
|
||||
return redirect(self.get_success_url())
|
||||
except ProtectedError as e:
|
||||
err = gettext('The organizer could not be deleted as some constraints (e.g. data created by plug-ins) do not allow it.')
|
||||
err = gettext(
|
||||
'The organizer could not be deleted as some constraints (e.g. data created by plug-ins) do not allow it.')
|
||||
|
||||
# Unlike deleting events (which is done by regular backend users), this feature can only be used by sysadmins,
|
||||
# so we expose more technical / less polished information.
|
||||
@@ -507,7 +512,8 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
|
||||
|
||||
@cached_property
|
||||
def footer_links_formset(self):
|
||||
return OrganizerFooterLinkFormset(self.request.POST if self.request.method == "POST" else None, organizer=self.object,
|
||||
return OrganizerFooterLinkFormset(self.request.POST if self.request.method == "POST" else None,
|
||||
organizer=self.object,
|
||||
prefix="footer-links", instance=self.object)
|
||||
|
||||
def save_footer_links_formset(self, obj):
|
||||
@@ -1328,6 +1334,95 @@ class WebHookLogsView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
|
||||
}))
|
||||
|
||||
|
||||
class GiftCardAcceptanceInviteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, FormView):
|
||||
model = GiftCardAcceptance
|
||||
template_name = 'pretixcontrol/organizers/giftcard_acceptance_invite.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
form_class = GiftCardAcceptanceInviteForm
|
||||
|
||||
def get_form_kwargs(self):
|
||||
return {
|
||||
**super().get_form_kwargs(),
|
||||
'organizer': self.request.organizer,
|
||||
}
|
||||
|
||||
def form_valid(self, form):
|
||||
self.request.organizer.gift_card_acceptor_acceptance.get_or_create(
|
||||
acceptor=form.cleaned_data['acceptor'],
|
||||
reusable_media=form.cleaned_data['reusable_media'],
|
||||
active=False,
|
||||
)
|
||||
self.request.organizer.log_action(
|
||||
'pretix.giftcards.acceptance.acceptor.invited',
|
||||
data={'acceptor': form.cleaned_data['acceptor'].slug,
|
||||
'reusable_media': form.cleaned_data['reusable_media']},
|
||||
user=self.request.user
|
||||
)
|
||||
messages.success(self.request, _('The selected organizer has been invited.'))
|
||||
return redirect(
|
||||
reverse('control:organizer.giftcards.acceptance', kwargs={'organizer': self.request.organizer.slug}))
|
||||
|
||||
|
||||
class GiftCardAcceptanceListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||
model = GiftCardAcceptance
|
||||
template_name = 'pretixcontrol/organizers/giftcard_acceptance_list.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'acceptor_acceptance'
|
||||
paginate_by = 50
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.organizer.gift_card_acceptor_acceptance.select_related(
|
||||
'acceptor'
|
||||
).order_by('acceptor__name', 'acceptor_id')
|
||||
return qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['issuer_acceptance'] = self.request.organizer.gift_card_issuer_acceptance.select_related(
|
||||
'issuer'
|
||||
)
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def post(self, request, *args, **kwargs):
|
||||
if "delete_acceptor" in request.POST:
|
||||
done = self.request.organizer.gift_card_acceptor_acceptance.filter(
|
||||
acceptor__slug=request.POST.get("delete_acceptor")
|
||||
).delete()
|
||||
if done:
|
||||
self.request.organizer.log_action(
|
||||
'pretix.giftcards.acceptance.acceptor.removed',
|
||||
data={'acceptor': request.POST.get("delete_acceptor")},
|
||||
user=request.user
|
||||
)
|
||||
messages.success(self.request, _('The selected connection has been removed.'))
|
||||
elif "delete_issuer" in request.POST:
|
||||
done = self.request.organizer.gift_card_issuer_acceptance.filter(
|
||||
issuer__slug=request.POST.get("delete_issuer")
|
||||
).delete()
|
||||
if done:
|
||||
self.request.organizer.log_action(
|
||||
'pretix.giftcards.acceptance.issuer.removed',
|
||||
data={'issuer': request.POST.get("delete_acceptor")},
|
||||
user=request.user
|
||||
)
|
||||
messages.success(self.request, _('The selected connection has been removed.'))
|
||||
if "accept_issuer" in request.POST:
|
||||
done = self.request.organizer.gift_card_issuer_acceptance.filter(
|
||||
issuer__slug=request.POST.get("accept_issuer")
|
||||
).update(active=True)
|
||||
if done:
|
||||
self.request.organizer.log_action(
|
||||
'pretix.giftcards.acceptance.issuer.accepted',
|
||||
data={'issuer': request.POST.get("accept_issuer")},
|
||||
user=request.user
|
||||
)
|
||||
messages.success(self.request, _('The selected connection has been accepted.'))
|
||||
|
||||
return redirect(
|
||||
reverse('control:organizer.giftcards.acceptance', kwargs={'organizer': self.request.organizer.slug}))
|
||||
|
||||
|
||||
class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||
model = GiftCard
|
||||
template_name = 'pretixcontrol/organizers/giftcards.html'
|
||||
@@ -1346,39 +1441,6 @@ class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
return qs
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if "add" in request.POST:
|
||||
o = self.request.user.get_organizers_with_permission(
|
||||
'can_manage_gift_cards', self.request
|
||||
).exclude(pk=self.request.organizer.pk).filter(
|
||||
slug=request.POST.get("add")
|
||||
).first()
|
||||
if o:
|
||||
self.request.organizer.gift_card_issuer_acceptance.get_or_create(
|
||||
issuer=o
|
||||
)
|
||||
self.request.organizer.log_action(
|
||||
'pretix.giftcards.acceptance.added',
|
||||
data={'issuer': o.slug},
|
||||
user=request.user
|
||||
)
|
||||
messages.success(self.request, _('The selected gift card issuer has been added.'))
|
||||
if "del" in request.POST:
|
||||
o = Organizer.objects.filter(
|
||||
slug=request.POST.get("del")
|
||||
).first()
|
||||
if o:
|
||||
self.request.organizer.gift_card_issuer_acceptance.filter(
|
||||
issuer=o
|
||||
).delete()
|
||||
self.request.organizer.log_action(
|
||||
'pretix.giftcards.acceptance.removed',
|
||||
data={'issuer': o.slug},
|
||||
user=request.user
|
||||
)
|
||||
messages.success(self.request, _('The selected gift card issuer has been removed.'))
|
||||
return redirect(reverse('control:organizer.giftcards', kwargs={'organizer': self.request.organizer.slug}))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['filter_form'] = self.filter_form
|
||||
@@ -1621,7 +1683,8 @@ class ExportMixin:
|
||||
def exporters(self):
|
||||
responses = register_multievent_data_exporters.send(self.request.organizer)
|
||||
raw_exporters = [
|
||||
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else self.events, self.request.organizer)
|
||||
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else self.events,
|
||||
self.request.organizer)
|
||||
for r, response in responses
|
||||
if response
|
||||
]
|
||||
@@ -1629,12 +1692,14 @@ class ExportMixin:
|
||||
ex for ex in raw_exporters
|
||||
if (
|
||||
not isinstance(ex, OrganizerLevelExportMixin) or
|
||||
self.request.user.has_organizer_permission(self.request.organizer, ex.organizer_required_permission, self.request)
|
||||
self.request.user.has_organizer_permission(self.request.organizer, ex.organizer_required_permission,
|
||||
self.request)
|
||||
)
|
||||
]
|
||||
return sorted(
|
||||
raw_exporters,
|
||||
key=lambda ex: (0 if ex.category else 1, ex.category or "", 0 if ex.featured else 1, str(ex.verbose_name).lower())
|
||||
key=lambda ex: (
|
||||
0 if ex.category else 1, ex.category or "", 0 if ex.featured else 1, str(ex.verbose_name).lower())
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@@ -1691,7 +1756,8 @@ class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, T
|
||||
data = self.scheduled.export_form_data
|
||||
else:
|
||||
if not self.exporter.form.is_valid():
|
||||
messages.error(self.request, _('There was a problem processing your input. See below for error details.'))
|
||||
messages.error(self.request,
|
||||
_('There was a problem processing your input. See below for error details.'))
|
||||
return self.get(request, *args, **kwargs)
|
||||
data = self.exporter.form.cleaned_data
|
||||
|
||||
@@ -1764,7 +1830,8 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
|
||||
else:
|
||||
initial = {}
|
||||
return RRuleForm(
|
||||
data=self.request.POST if self.request.method == 'POST' and self.request.POST.get("schedule") == "save" else None,
|
||||
data=self.request.POST if self.request.method == 'POST' and self.request.POST.get(
|
||||
"schedule") == "save" else None,
|
||||
prefix="rrule",
|
||||
initial=initial
|
||||
)
|
||||
@@ -1779,7 +1846,8 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
|
||||
if not self.scheduled:
|
||||
initial = {
|
||||
"mail_subject": gettext("Export: {title}").format(title=self.exporter.verbose_name),
|
||||
"mail_template": gettext("Hello,\n\nattached to this email, you can find a new scheduled report for {name}.").format(
|
||||
"mail_template": gettext(
|
||||
"Hello,\n\nattached to this email, you can find a new scheduled report for {name}.").format(
|
||||
name=str(self.request.organizer.name)
|
||||
),
|
||||
"schedule_rrule_time": time(4, 0, 0),
|
||||
@@ -1787,7 +1855,8 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
|
||||
else:
|
||||
initial = {}
|
||||
return ScheduledOrganizerExportForm(
|
||||
data=self.request.POST if self.request.method == 'POST' and self.request.POST.get("schedule") == "save" else None,
|
||||
data=self.request.POST if self.request.method == 'POST' and self.request.POST.get(
|
||||
"schedule") == "save" else None,
|
||||
prefix="schedule",
|
||||
instance=instance,
|
||||
initial=initial,
|
||||
@@ -1804,7 +1873,8 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
|
||||
elif not self.exporter:
|
||||
for s in ctx['scheduled']:
|
||||
try:
|
||||
s.export_verbose_name = [e for e in self.exporters if e.identifier == s.export_identifier][0].verbose_name
|
||||
s.export_verbose_name = [e for e in self.exporters if e.identifier == s.export_identifier][
|
||||
0].verbose_name
|
||||
except IndexError:
|
||||
s.export_verbose_name = "?"
|
||||
return ctx
|
||||
@@ -2236,9 +2306,10 @@ class SSOProviderUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequire
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['redirect_uri'] = build_absolute_uri(self.request.organizer, 'presale:organizer.customer.login.return', kwargs={
|
||||
'provider': self.object.pk
|
||||
})
|
||||
ctx['redirect_uri'] = build_absolute_uri(self.request.organizer, 'presale:organizer.customer.login.return',
|
||||
kwargs={
|
||||
'provider': self.object.pk
|
||||
})
|
||||
return ctx
|
||||
|
||||
def get_form_kwargs(self):
|
||||
|
||||
@@ -293,6 +293,19 @@ def test_giftcard_transact_cross_organizer(token_client, organizer, event, other
|
||||
assert other_giftcard.transactions.last().acceptor == organizer
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_giftcard_transact_cross_organizer_inactive(token_client, organizer, event, other_giftcard):
|
||||
organizer.gift_card_issuer_acceptance.update(active=False)
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/giftcards/{}/transact/?include_accepted=true'.format(organizer.slug, other_giftcard.pk),
|
||||
{
|
||||
'value': '10.00',
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_giftcard_transact_min_zero(token_client, organizer, event, giftcard):
|
||||
resp = token_client.post(
|
||||
|
||||
@@ -19,14 +19,16 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.models import Order, Organizer, ReusableMedium
|
||||
from pretix.base.models import (
|
||||
Event, GiftCardAcceptance, Order, Organizer, ReusableMedium,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -49,11 +51,28 @@ def organizer2():
|
||||
|
||||
@pytest.fixture
|
||||
def giftcard2(organizer2):
|
||||
gc = organizer2.issued_gift_cards.create(secret="ABCDEF", currency="EUR")
|
||||
gc = organizer2.issued_gift_cards.create(secret="IJKLMNOP", currency="EUR")
|
||||
gc.transactions.create(value=Decimal('23.00'), acceptor=organizer2)
|
||||
return gc
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def medium2(organizer2):
|
||||
m = organizer2.reusable_media.create(identifier="ABCDEFGH", type="barcode", active=True)
|
||||
return m
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@scopes_disabled()
|
||||
def org2_event(organizer2):
|
||||
e = Event.objects.create(
|
||||
organizer=organizer2, name='Dummy2', slug='dummy2',
|
||||
date_from=datetime(2017, 12, 27, 10, 0, 0, tzinfo=timezone.utc),
|
||||
plugins='pretix.plugins.banktransfer,pretix.plugins.ticketoutputpdf'
|
||||
)
|
||||
return e
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def customer(organizer, event):
|
||||
return organizer.customers.create(
|
||||
@@ -67,6 +86,7 @@ def customer(organizer, event):
|
||||
|
||||
TEST_MEDIUM_RES = {
|
||||
"id": 1,
|
||||
"organizer": "dummy",
|
||||
"identifier": "ABCDEFGH",
|
||||
"type": "barcode",
|
||||
"active": True,
|
||||
@@ -357,3 +377,64 @@ def test_medium_autocreate(token_client, organizer):
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data["result"] is None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_medium_lookup_cross_organizer(token_client, organizer, organizer2, org2_event, medium2, giftcard2):
|
||||
with scopes_disabled():
|
||||
o = Order.objects.create(
|
||||
code='FOO', event=org2_event, email='dummy@dummy.test',
|
||||
status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10),
|
||||
total=14, locale='en'
|
||||
)
|
||||
ticket = org2_event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
|
||||
personalized=True)
|
||||
op = o.positions.create(item=ticket, price=Decimal("14"))
|
||||
medium2.linked_orderposition = op
|
||||
medium2.linked_giftcard = giftcard2
|
||||
medium2.save()
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/reusablemedia/lookup/'.format(organizer.slug),
|
||||
{
|
||||
"type": medium2.type,
|
||||
"identifier": medium2.identifier,
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data["result"] is None
|
||||
|
||||
gca = GiftCardAcceptance.objects.create(
|
||||
issuer=organizer2,
|
||||
acceptor=organizer,
|
||||
active=True,
|
||||
reusable_media=False
|
||||
)
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/reusablemedia/lookup/'.format(organizer.slug),
|
||||
{
|
||||
"type": medium2.type,
|
||||
"identifier": medium2.identifier,
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data["result"] is None
|
||||
|
||||
gca.reusable_media = True
|
||||
gca.save()
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/reusablemedia/lookup/'.format(organizer.slug),
|
||||
{
|
||||
"type": medium2.type,
|
||||
"identifier": medium2.identifier,
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data["result"] is not None
|
||||
assert resp.data["result"]["organizer"] == "partner"
|
||||
assert resp.data["result"]["linked_giftcard"] is not None
|
||||
assert resp.data["result"]["linked_orderposition"] is None
|
||||
|
||||
@@ -51,7 +51,8 @@ def gift_card(organizer):
|
||||
@pytest.fixture
|
||||
def admin_user(organizer):
|
||||
u = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
||||
admin_team = Team.objects.create(organizer=organizer, can_manage_gift_cards=True, name='Admin team')
|
||||
admin_team = Team.objects.create(organizer=organizer, can_manage_gift_cards=True, name='Admin team',
|
||||
can_change_organizer_settings=True)
|
||||
admin_team.members.add(u)
|
||||
return u
|
||||
|
||||
@@ -174,24 +175,29 @@ def test_card_detail_view_transact_invalid_value(organizer, admin_user, gift_car
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_manage_acceptance(organizer, organizer2, admin_user, gift_card, client, team2):
|
||||
gca = organizer.gift_card_issuer_acceptance.create(issuer=organizer2, active=False)
|
||||
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
client.post('/control/organizer/dummy/giftcards', {
|
||||
'add': organizer2.slug
|
||||
client.post('/control/organizer/dummy/giftcards/acceptance', {
|
||||
'accept_issuer': organizer2.slug
|
||||
})
|
||||
assert organizer.gift_card_issuer_acceptance.filter(issuer=organizer2).exists()
|
||||
client.post('/control/organizer/dummy/giftcards', {
|
||||
'del': organizer2.slug
|
||||
|
||||
gca.refresh_from_db()
|
||||
assert gca.active
|
||||
|
||||
client.post('/control/organizer/dummy/giftcards/acceptance', {
|
||||
'delete_issuer': organizer2.slug
|
||||
})
|
||||
assert not organizer.gift_card_issuer_acceptance.filter(issuer=organizer2).exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_manage_acceptance_permission_required(organizer, organizer2, admin_user, gift_card, client):
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
client.post('/control/organizer/dummy/giftcards', {
|
||||
'add': organizer2.slug
|
||||
client.post('/control/organizer/dummy/giftcards/acceptance/invite', {
|
||||
'acceptor': organizer2.slug
|
||||
})
|
||||
assert not organizer.gift_card_issuer_acceptance.filter(issuer=organizer2).exists()
|
||||
assert organizer.gift_card_acceptor_acceptance.filter(acceptor=organizer2).exists()
|
||||
client.post('/control/organizer/dummy/giftcards/acceptance', {
|
||||
'delete_acceptor': organizer2.slug
|
||||
})
|
||||
assert not organizer.gift_card_acceptor_acceptance.filter(acceptor=organizer2).exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
@@ -219,6 +219,8 @@ organizer_urls = [
|
||||
'organizer/abc/giftcard/add',
|
||||
'organizer/abc/giftcard/1/',
|
||||
'organizer/abc/giftcard/1/edit',
|
||||
'organizer/abc/giftcards/acceptance',
|
||||
'organizer/abc/giftcards/acceptance/invite',
|
||||
]
|
||||
|
||||
|
||||
@@ -552,6 +554,8 @@ organizer_permission_urls = [
|
||||
("can_manage_gift_cards", "organizer/dummy/giftcard/add", 200),
|
||||
("can_manage_gift_cards", "organizer/dummy/giftcard/1/", 404),
|
||||
("can_manage_gift_cards", "organizer/dummy/giftcard/1/edit", 404),
|
||||
("can_change_organizer_settings", "organizer/dummy/giftcards/acceptance", 200),
|
||||
("can_change_organizer_settings", "organizer/dummy/giftcards/acceptance/invite", 200),
|
||||
|
||||
# bank transfer
|
||||
("can_change_orders", "organizer/dummy/banktransfer/import/", 200),
|
||||
|
||||
@@ -1676,6 +1676,24 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
|
||||
assert gc.issuer == orga2
|
||||
assert gc.transactions.last().acceptor == self.orga
|
||||
|
||||
def test_giftcard_cross_organizer_inactive(self):
|
||||
self.orga.issued_gift_cards.create(currency="EUR")
|
||||
orga2 = Organizer.objects.create(slug="foo2", name="foo2")
|
||||
gc = orga2.issued_gift_cards.create(currency="EUR")
|
||||
gc.transactions.create(value=23, acceptor=orga2)
|
||||
self.orga.gift_card_issuer_acceptance.create(issuer=orga2, active=False)
|
||||
self.event.settings.set('payment_banktransfer__enabled', True)
|
||||
with scopes_disabled():
|
||||
CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=23, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
response = self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), {
|
||||
'payment': 'giftcard',
|
||||
'giftcard': gc.secret
|
||||
}, follow=True)
|
||||
assert 'This gift card is not known.' in response.content.decode()
|
||||
|
||||
def test_giftcard_in_test_mode(self):
|
||||
gc = self.orga.issued_gift_cards.create(currency="EUR")
|
||||
gc.transactions.create(value=20, acceptor=self.orga)
|
||||
|
||||
Reference in New Issue
Block a user