Gift cards: Improved support for cross-organizer acceptance (#3311)

Co-authored-by: Martin Gross <martin@pc-coholic.de>
This commit is contained in:
Raphael Michel
2023-06-15 14:17:40 +02:00
committed by GitHub
co-authored by Martin Gross
parent b3c917925c
commit f8be8296dd
22 changed files with 605 additions and 139 deletions
+9
View File
@@ -19,6 +19,7 @@ Field Type Description
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
id integer Internal ID of the medium id integer Internal ID of the medium
type string Type of medium, e.g. ``"barcode"`` or ``"nfc_uid"``. type string Type of medium, e.g. ``"barcode"`` or ``"nfc_uid"``.
organizer string Organizer slug of the organizer who "owns" this medium.
identifier string Unique identifier of the medium. The format depends on the ``type``. identifier string Unique identifier of the medium. The format depends on the ``type``.
active boolean Whether this medium may be used. active boolean Whether this medium may be used.
created datetime Date of creation created datetime Date of creation
@@ -67,6 +68,7 @@ Endpoints
"results": [ "results": [
{ {
"id": 1, "id": 1,
"organizer": "bigevents",
"identifier": "ABCDEFGH", "identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z", "created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z", "updated": "2021-04-06T13:44:22.809377Z",
@@ -123,6 +125,7 @@ Endpoints
{ {
"id": 1, "id": 1,
"organizer": "bigevents",
"identifier": "ABCDEFGH", "identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z", "created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z", "updated": "2021-04-06T13:44:22.809377Z",
@@ -152,6 +155,9 @@ Endpoints
Look up a new reusable medium by its identifier. In some cases, this might lead to the automatic creation of a new Look up a new reusable medium by its identifier. In some cases, this might lead to the automatic creation of a new
medium behind the scenes. medium behind the scenes.
This endpoint, and this endpoint only, might return media from a different organizer if there is a cross-acceptance
agreement. In this case, only linked gift cards will be returned, no order position or customer records,
**Example request**: **Example request**:
.. sourcecode:: http .. sourcecode:: http
@@ -176,6 +182,7 @@ Endpoints
{ {
"id": 1, "id": 1,
"organizer": "bigevents",
"identifier": "ABCDEFGH", "identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z", "created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z", "updated": "2021-04-06T13:44:22.809377Z",
@@ -235,6 +242,7 @@ Endpoints
{ {
"id": 1, "id": 1,
"organizer": "bigevents",
"identifier": "ABCDEFGH", "identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z", "created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z", "updated": "2021-04-06T13:44:22.809377Z",
@@ -291,6 +299,7 @@ Endpoints
{ {
"id": 1, "id": 1,
"organizer": "bigevents",
"identifier": "ABCDEFGH", "identifier": "ABCDEFGH",
"created": "2021-04-06T13:44:22.809377Z", "created": "2021-04-06T13:44:22.809377Z",
"updated": "2021-04-06T13:44:22.809377Z", "updated": "2021-04-06T13:44:22.809377Z",
+3
View File
@@ -60,6 +60,8 @@ class NestedGiftCardSerializer(GiftCardSerializer):
class ReusableMediaSerializer(I18nAwareModelSerializer): class ReusableMediaSerializer(I18nAwareModelSerializer):
organizer = serializers.SlugRelatedField(slug_field='slug', read_only=True)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -111,6 +113,7 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
model = ReusableMedium model = ReusableMedium
fields = ( fields = (
'id', 'id',
'organizer',
'created', 'created',
'updated', 'updated',
'type', 'type',
+8 -5
View File
@@ -36,9 +36,9 @@ from pretix.api.serializers.settings import SettingsSerializer
from pretix.base.auth import get_auth_backends from pretix.base.auth import get_auth_backends
from pretix.base.i18n import get_language_without_region from pretix.base.i18n import get_language_without_region
from pretix.base.models import ( from pretix.base.models import (
Customer, Device, GiftCard, GiftCardTransaction, Membership, Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction,
MembershipType, OrderPosition, Organizer, ReusableMedium, SeatingPlan, Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
Team, TeamAPIToken, TeamInvite, User, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
) )
from pretix.base.models.seating import SeatingPlanLayoutValidator from pretix.base.models.seating import SeatingPlanLayoutValidator
from pretix.base.services.mail import SendMailException, mail from pretix.base.services.mail import SendMailException, mail
@@ -183,8 +183,11 @@ class GiftCardSerializer(I18nAwareModelSerializer):
qs = GiftCard.objects.filter( qs = GiftCard.objects.filter(
secret=s secret=s
).filter( ).filter(
Q(issuer=self.context["organizer"]) | Q( Q(issuer=self.context["organizer"]) |
issuer__gift_card_collector_acceptance__collector=self.context["organizer"]) Q(issuer__in=GiftCardAcceptance.objects.filter(
acceptor=self.context["organizer"],
active=True,
).values_list('issuer', flat=True))
) )
if self.instance: if self.instance:
qs = qs.exclude(pk=self.instance.pk) qs = qs.exclude(pk=self.instance.pk)
+24 -7
View File
@@ -39,7 +39,8 @@ from pretix.api.serializers.media import (
) )
from pretix.base.media import MEDIA_TYPES from pretix.base.media import MEDIA_TYPES
from pretix.base.models import ( 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 import OF_SELF
from pretix.helpers.dicts import merge_dicts from pretix.helpers.dicts import merge_dicts
@@ -135,12 +136,28 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
s = self.get_serializer(m) s = self.get_serializer(m)
return Response({"result": s.data}) return Response({"result": s.data})
except ReusableMedium.DoesNotExist: except ReusableMedium.DoesNotExist:
mt = MEDIA_TYPES.get(s.validated_data["type"]) try:
if mt: with scopes_disabled():
m = mt.handle_unknown(request.organizer, s.validated_data["identifier"], request.user, request.auth) m = ReusableMedium.objects.get(
if m: organizer__in=GiftCardAcceptance.objects.filter(
s = self.get_serializer(m) acceptor=request.organizer,
return Response({"result": s.data}) 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}) return Response({"result": None})
+3 -1
View File
@@ -1147,7 +1147,7 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
def iterate_list(self, form_data): def iterate_list(self, form_data):
qs = GiftCardTransaction.objects.filter( qs = GiftCardTransaction.objects.filter(
card__issuer=self.organizer, 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'): 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) 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'), _('Amount'),
_('Currency'), _('Currency'),
_('Order'), _('Order'),
_('Organizer'),
] ]
yield headers yield headers
@@ -1174,6 +1175,7 @@ class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
obj.value, obj.value,
obj.card.currency, obj.card.currency,
obj.order.full_code if obj.order else None, obj.order.full_code if obj.order else None,
str(obj.acceptor or ""),
] ]
yield row yield row
@@ -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')},
),
]
+8 -3
View File
@@ -46,14 +46,19 @@ def gen_giftcard_secret(length=8):
class GiftCardAcceptance(models.Model): class GiftCardAcceptance(models.Model):
issuer = models.ForeignKey( issuer = models.ForeignKey(
'Organizer', 'Organizer',
related_name='gift_card_collector_acceptance', related_name='gift_card_acceptor_acceptance',
on_delete=models.CASCADE on_delete=models.CASCADE
) )
collector = models.ForeignKey( acceptor = models.ForeignKey(
'Organizer', 'Organizer',
related_name='gift_card_issuer_acceptance', related_name='gift_card_issuer_acceptance',
on_delete=models.CASCADE on_delete=models.CASCADE
) )
active = models.BooleanField(default=True)
reusable_media = models.BooleanField(default=False)
class Meta:
unique_together = (('issuer', 'acceptor'),)
class GiftCard(LoggedModel): class GiftCard(LoggedModel):
@@ -114,7 +119,7 @@ class GiftCard(LoggedModel):
return self.transactions.aggregate(s=Sum('value'))['s'] or Decimal('0.00') return self.transactions.aggregate(s=Sum('value'))['s'] or Decimal('0.00')
def accepted_by(self, organizer): 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): def save(self, *args, **kwargs):
if not self.secret: if not self.secret:
+8 -6
View File
@@ -40,7 +40,7 @@ from django.conf import settings
from django.core.mail import get_connection from django.core.mail import get_connection
from django.core.validators import MinLengthValidator, RegexValidator from django.core.validators import MinLengthValidator, RegexValidator
from django.db import models 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.urls import reverse
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.functional import cached_property from django.utils.functional import cached_property
@@ -157,17 +157,19 @@ class Organizer(LoggedModel):
return self.cache.get_or_set( return self.cache.get_or_set(
key='has_gift_cards', key='has_gift_cards',
timeout=15, 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 @property
def accepted_gift_cards(self): def accepted_gift_cards(self):
from .giftcards import GiftCard, GiftCardAcceptance from .giftcards import GiftCard, GiftCardAcceptance
return GiftCard.objects.annotate( return GiftCard.objects.filter(
accepted=Exists(GiftCardAcceptance.objects.filter(issuer=OuterRef('issuer'), collector=self)) Q(issuer=self) |
).filter( Q(issuer__in=GiftCardAcceptance.objects.filter(
Q(issuer=self) | Q(accepted=True) acceptor=self,
active=True,
).values_list('issuer', flat=True))
) )
@property @property
+36 -3
View File
@@ -65,8 +65,8 @@ from pretix.base.forms.questions import (
) )
from pretix.base.forms.widgets import SplitDateTimePickerWidget from pretix.base.forms.widgets import SplitDateTimePickerWidget
from pretix.base.models import ( from pretix.base.models import (
Customer, Device, EventMetaProperty, Gate, GiftCard, Membership, Customer, Device, EventMetaProperty, Gate, GiftCard, GiftCardAcceptance,
MembershipType, OrderPosition, Organizer, ReusableMedium, Team, Membership, MembershipType, OrderPosition, Organizer, ReusableMedium, Team,
) )
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
from pretix.base.models.organizer import OrganizerFooterLink from pretix.base.models.organizer import OrganizerFooterLink
@@ -637,7 +637,11 @@ class GiftCardCreateForm(forms.ModelForm):
if GiftCard.objects.filter( if GiftCard.objects.filter(
secret__iexact=s secret__iexact=s
).filter( ).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(): ).exists():
raise ValidationError( raise ValidationError(
_('A gift card with the same secret already exists in your or an affiliated organizer account.') _('A gift card with the same secret already exists in your or an affiliated organizer account.')
@@ -1026,3 +1030,32 @@ class SSOClientForm(I18nModelForm):
else: else:
del self.fields['client_id'] del self.fields['client_id']
del self.fields['regenerate_client_secret'] 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
+3
View File
@@ -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.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.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.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.created': _('The webhook has been created.'),
'pretix.webhook.changed': _('The webhook has been changed.'), 'pretix.webhook.changed': _('The webhook has been changed.'),
'pretix.webhook.retries.expedited': _('The webhook call retry jobs have been manually expedited.'), 'pretix.webhook.retries.expedited': _('The webhook call retry jobs have been manually expedited.'),
+20 -1
View File
@@ -519,13 +519,32 @@ def get_organizer_navigation(request):
}) })
if 'can_manage_gift_cards' in request.orgapermset: 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({ nav.append({
'label': _('Gift cards'), 'label': _('Gift cards'),
'url': reverse('control:organizer.giftcards', kwargs={ 'url': reverse('control:organizer.giftcards', kwargs={
'organizer': request.organizer.slug 'organizer': request.organizer.slug
}), }),
'active': 'organizer.giftcard' in url.url_name,
'icon': 'credit-card', 'icon': 'credit-card',
'children': children,
}) })
if request.organizer.settings.customer_accounts: if request.organizer.settings.customer_accounts:
@@ -6,6 +6,12 @@
<a href="{% url "control:organizer.giftcard" organizer=gc.issuer.slug giftcard=gc.pk %}"> <a href="{% url "control:organizer.giftcard" organizer=gc.issuer.slug giftcard=gc.pk %}">
{{ gc.secret }} {{ gc.secret }}
</a> </a>
{% if gc.issuer != request.organizer %}
<span class="text-muted">
<br>
<span class="fa fa-group"></span> {{ gc.issuer }}
</span>
{% endif %}
</dd> </dd>
<dt>{% trans "Issuer" %}</dt> <dt>{% trans "Issuer" %}</dt>
<dd>{{ gc.issuer }}</dd> <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> </div>
{% include "pretixcontrol/pagination.html" %} {% include "pretixcontrol/pagination.html" %}
{% endif %} {% 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 %} {% endblock %}
+4
View File
@@ -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>[^/]+)/$', organizer.GiftCardDetailView.as_view(), name='organizer.giftcard'),
re_path(r'^organizer/(?P<organizer>[^/]+)/giftcard/(?P<giftcard>[^/]+)/edit$', organizer.GiftCardUpdateView.as_view(), re_path(r'^organizer/(?P<organizer>[^/]+)/giftcard/(?P<giftcard>[^/]+)/edit$', organizer.GiftCardUpdateView.as_view(),
name='organizer.giftcard.edit'), 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>[^/]+)/webhooks$', organizer.WebHookListView.as_view(), name='organizer.webhooks'),
re_path(r'^organizer/(?P<organizer>[^/]+)/webhook/add$', organizer.WebHookCreateView.as_view(), re_path(r'^organizer/(?P<organizer>[^/]+)/webhook/add$', organizer.WebHookCreateView.as_view(),
name='organizer.webhook.add'), name='organizer.webhook.add'),
+128 -57
View File
@@ -77,7 +77,7 @@ from pretix.base.models import (
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
from pretix.base.models.event import Event, EventMetaProperty, EventMetaValue from pretix.base.models.event import Event, EventMetaProperty, EventMetaValue
from pretix.base.models.giftcards import ( 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.orders import CancellationRequest
from pretix.base.models.organizer import TeamAPIToken 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.orders import ExporterForm
from pretix.control.forms.organizer import ( from pretix.control.forms.organizer import (
CustomerCreateForm, CustomerUpdateForm, DeviceBulkEditForm, DeviceForm, CustomerCreateForm, CustomerUpdateForm, DeviceBulkEditForm, DeviceForm,
EventMetaPropertyForm, GateForm, GiftCardCreateForm, GiftCardUpdateForm, EventMetaPropertyForm, GateForm, GiftCardAcceptanceInviteForm,
MailSettingsForm, MembershipTypeForm, MembershipUpdateForm, GiftCardCreateForm, GiftCardUpdateForm, MailSettingsForm,
OrganizerDeleteForm, OrganizerFooterLinkFormset, OrganizerForm, MembershipTypeForm, MembershipUpdateForm, OrganizerDeleteForm,
OrganizerSettingsForm, OrganizerUpdateForm, ReusableMediumCreateForm, OrganizerFooterLinkFormset, OrganizerForm, OrganizerSettingsForm,
ReusableMediumUpdateForm, SSOClientForm, SSOProviderForm, TeamForm, OrganizerUpdateForm, ReusableMediumCreateForm, ReusableMediumUpdateForm,
WebHookForm, SSOClientForm, SSOProviderForm, TeamForm, WebHookForm,
) )
from pretix.control.forms.rrule import RRuleForm from pretix.control.forms.rrule import RRuleForm
from pretix.control.logdisplay import OVERVIEW_BANLIST from pretix.control.logdisplay import OVERVIEW_BANLIST
@@ -181,7 +181,8 @@ class OrganizerDetail(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
return self.request.organizer return self.request.organizer
def get_queryset(self): 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', '_settings_objects', 'organizer___settings_objects',
'organizer__meta_properties', 'organizer__meta_properties',
Prefetch( Prefetch(
@@ -211,7 +212,8 @@ class OrganizerDetail(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['filter_form'] = self.filter_form ctx['filter_form'] = self.filter_form
ctx['meta_fields'] = [ 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 return ctx
@@ -316,7 +318,8 @@ class MailSettingsPreview(OrganizerPermissionRequiredMixin, View):
# get all supported placeholders with dummy values # get all supported placeholders with dummy values
def placeholders(self, item): def placeholders(self, item):
ctx = {} 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('*'): if s.strip().startswith('*'):
ctx[p] = s ctx[p] = s
else: else:
@@ -341,7 +344,8 @@ class MailSettingsPreview(OrganizerPermissionRequiredMixin, View):
if idx in self.supported_locale: if idx in self.supported_locale:
with language(self.supported_locale[idx], self.request.organizer.settings.region): with language(self.supported_locale[idx], self.request.organizer.settings.region):
if k.startswith('mail_subject_'): 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: else:
msgs[self.supported_locale[idx]] = markdown_compile_email( msgs[self.supported_locale[idx]] = markdown_compile_email(
format_map(v, self.placeholders(preview_item)) format_map(v, self.placeholders(preview_item))
@@ -395,7 +399,8 @@ class OrganizerDelete(AdministratorPermissionRequiredMixin, FormView):
messages.success(self.request, _('The organizer has been deleted.')) messages.success(self.request, _('The organizer has been deleted.'))
return redirect(self.get_success_url()) return redirect(self.get_success_url())
except ProtectedError as e: 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, # 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. # so we expose more technical / less polished information.
@@ -507,7 +512,8 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
@cached_property @cached_property
def footer_links_formset(self): 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) prefix="footer-links", instance=self.object)
def save_footer_links_formset(self, obj): 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): class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = GiftCard model = GiftCard
template_name = 'pretixcontrol/organizers/giftcards.html' template_name = 'pretixcontrol/organizers/giftcards.html'
@@ -1346,39 +1441,6 @@ class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
qs = self.filter_form.filter_qs(qs) qs = self.filter_form.filter_qs(qs)
return 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): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['filter_form'] = self.filter_form ctx['filter_form'] = self.filter_form
@@ -1621,7 +1683,8 @@ class ExportMixin:
def exporters(self): def exporters(self):
responses = register_multievent_data_exporters.send(self.request.organizer) responses = register_multievent_data_exporters.send(self.request.organizer)
raw_exporters = [ 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 for r, response in responses
if response if response
] ]
@@ -1629,12 +1692,14 @@ class ExportMixin:
ex for ex in raw_exporters ex for ex in raw_exporters
if ( if (
not isinstance(ex, OrganizerLevelExportMixin) or 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( return sorted(
raw_exporters, 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): def get_context_data(self, **kwargs):
@@ -1691,7 +1756,8 @@ class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, T
data = self.scheduled.export_form_data data = self.scheduled.export_form_data
else: else:
if not self.exporter.form.is_valid(): 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) return self.get(request, *args, **kwargs)
data = self.exporter.form.cleaned_data data = self.exporter.form.cleaned_data
@@ -1764,7 +1830,8 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
else: else:
initial = {} initial = {}
return RRuleForm( 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", prefix="rrule",
initial=initial initial=initial
) )
@@ -1779,7 +1846,8 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
if not self.scheduled: if not self.scheduled:
initial = { initial = {
"mail_subject": gettext("Export: {title}").format(title=self.exporter.verbose_name), "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) name=str(self.request.organizer.name)
), ),
"schedule_rrule_time": time(4, 0, 0), "schedule_rrule_time": time(4, 0, 0),
@@ -1787,7 +1855,8 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
else: else:
initial = {} initial = {}
return ScheduledOrganizerExportForm( 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", prefix="schedule",
instance=instance, instance=instance,
initial=initial, initial=initial,
@@ -1804,7 +1873,8 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
elif not self.exporter: elif not self.exporter:
for s in ctx['scheduled']: for s in ctx['scheduled']:
try: 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: except IndexError:
s.export_verbose_name = "?" s.export_verbose_name = "?"
return ctx return ctx
@@ -2236,9 +2306,10 @@ class SSOProviderUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequire
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['redirect_uri'] = build_absolute_uri(self.request.organizer, 'presale:organizer.customer.login.return', kwargs={ ctx['redirect_uri'] = build_absolute_uri(self.request.organizer, 'presale:organizer.customer.login.return',
'provider': self.object.pk kwargs={
}) 'provider': self.object.pk
})
return ctx return ctx
def get_form_kwargs(self): def get_form_kwargs(self):
+13
View File
@@ -293,6 +293,19 @@ def test_giftcard_transact_cross_organizer(token_client, organizer, event, other
assert other_giftcard.transactions.last().acceptor == organizer 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 @pytest.mark.django_db
def test_giftcard_transact_min_zero(token_client, organizer, event, giftcard): def test_giftcard_transact_min_zero(token_client, organizer, event, giftcard):
resp = token_client.post( resp = token_client.post(
+84 -3
View File
@@ -19,14 +19,16 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # 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/>. # <https://www.gnu.org/licenses/>.
# #
from datetime import timedelta from datetime import datetime, timedelta, timezone
from decimal import Decimal from decimal import Decimal
import pytest import pytest
from django.utils.timezone import now from django.utils.timezone import now
from django_scopes import scopes_disabled 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 @pytest.fixture
@@ -49,11 +51,28 @@ def organizer2():
@pytest.fixture @pytest.fixture
def giftcard2(organizer2): 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) gc.transactions.create(value=Decimal('23.00'), acceptor=organizer2)
return gc 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 @pytest.fixture
def customer(organizer, event): def customer(organizer, event):
return organizer.customers.create( return organizer.customers.create(
@@ -67,6 +86,7 @@ def customer(organizer, event):
TEST_MEDIUM_RES = { TEST_MEDIUM_RES = {
"id": 1, "id": 1,
"organizer": "dummy",
"identifier": "ABCDEFGH", "identifier": "ABCDEFGH",
"type": "barcode", "type": "barcode",
"active": True, "active": True,
@@ -357,3 +377,64 @@ def test_medium_autocreate(token_client, organizer):
) )
assert resp.status_code == 200 assert resp.status_code == 200
assert resp.data["result"] is None 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
+19 -13
View File
@@ -51,7 +51,8 @@ def gift_card(organizer):
@pytest.fixture @pytest.fixture
def admin_user(organizer): def admin_user(organizer):
u = User.objects.create_user('dummy@dummy.dummy', 'dummy') 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) admin_team.members.add(u)
return u return u
@@ -174,24 +175,29 @@ def test_card_detail_view_transact_invalid_value(organizer, admin_user, gift_car
@pytest.mark.django_db @pytest.mark.django_db
def test_manage_acceptance(organizer, organizer2, admin_user, gift_card, client, team2): 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.login(email='dummy@dummy.dummy', password='dummy')
client.post('/control/organizer/dummy/giftcards', { client.post('/control/organizer/dummy/giftcards/acceptance', {
'add': organizer2.slug 'accept_issuer': organizer2.slug
}) })
assert organizer.gift_card_issuer_acceptance.filter(issuer=organizer2).exists()
client.post('/control/organizer/dummy/giftcards', { gca.refresh_from_db()
'del': organizer2.slug 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() assert not organizer.gift_card_issuer_acceptance.filter(issuer=organizer2).exists()
client.post('/control/organizer/dummy/giftcards/acceptance/invite', {
@pytest.mark.django_db 'acceptor': organizer2.slug
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
}) })
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 @pytest.mark.django_db
+4
View File
@@ -219,6 +219,8 @@ organizer_urls = [
'organizer/abc/giftcard/add', 'organizer/abc/giftcard/add',
'organizer/abc/giftcard/1/', 'organizer/abc/giftcard/1/',
'organizer/abc/giftcard/1/edit', '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/add", 200),
("can_manage_gift_cards", "organizer/dummy/giftcard/1/", 404), ("can_manage_gift_cards", "organizer/dummy/giftcard/1/", 404),
("can_manage_gift_cards", "organizer/dummy/giftcard/1/edit", 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 # bank transfer
("can_change_orders", "organizer/dummy/banktransfer/import/", 200), ("can_change_orders", "organizer/dummy/banktransfer/import/", 200),
+18
View File
@@ -1676,6 +1676,24 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
assert gc.issuer == orga2 assert gc.issuer == orga2
assert gc.transactions.last().acceptor == self.orga 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): def test_giftcard_in_test_mode(self):
gc = self.orga.issued_gift_cards.create(currency="EUR") gc = self.orga.issued_gift_cards.create(currency="EUR")
gc.transactions.create(value=20, acceptor=self.orga) gc.transactions.create(value=20, acceptor=self.orga)