Move auto check-in to plugin with more functionality (#4331)

* Move auto check-in to plugin with more functionality

* Rename field

* Add to MANIFEST.in
This commit is contained in:
Raphael Michel
2024-07-29 09:46:53 +02:00
committed by GitHub
parent c6a2ae3783
commit cab360bdb6
28 changed files with 2285 additions and 6 deletions

View File

@@ -10,6 +10,8 @@ recursive-include src/pretix/helpers/locale *
recursive-include src/pretix/base/templates *
recursive-include src/pretix/control/templates *
recursive-include src/pretix/presale/templates *
recursive-include src/pretix/plugins/autocheckin/templates *
recursive-include src/pretix/plugins/autocheckin/static *
recursive-include src/pretix/plugins/banktransfer/templates *
recursive-include src/pretix/plugins/banktransfer/static *
recursive-include src/pretix/plugins/manualpayment/templates *

View File

@@ -0,0 +1,259 @@
.. _rest-autocheckinrules:
Auto check-in rules
===================
This feature requires the bundled ``pretix.plugins.autocheckin`` plugin to be active for the event in order to work properly.
Resource description
--------------------
Auto check-in rules specify that tickets should under specific conditions automatically be considered checked in after
they have been purchased.
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the rule
list integer ID of the check-in list to check the ticket in on. If
``None``, the system will select all matching check-in lists.
mode string ``"placed"`` if the rule should be evaluated right after
an order has been created, ``"paid"`` if the rule should
be evaluated after the order has been fully paid.
all_sales_channels boolean If ``true`` (default), the rule applies to tickets sold on all sales channels.
limit_sales_channels list of strings List of sales channel identifiers the rule should apply to
if ``all_sales_channels`` is ``false``.
all_products boolean If ``true`` (default), the rule affects all products and variations.
limit_products list of integers List of item IDs, if ``all_products`` is not set. If the
product listed here has variations, all variations will be matched.
limit_variations list of integers List of product variation IDs, if ``all_products`` is not set.
The parent product does not need to be part of ``limit_products``.
all_payment_methods boolean If ``true`` (default), the rule applies to tickets paid with all payment methods.
limit_payment_methods list of strings List of payment method identifiers the rule should apply to
if ``all_payment_methods`` is ``false``.
===================================== ========================== =======================================================
.. versionadded:: 2024.7
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/
Returns a list of all rules configured for an event.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"list": 12345,
"mode": "placed",
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"all_products": False,
"limit_products": [2, 3],
"limit_variations": [456],
"all_payment_methods": true,
"limit_payment_methods": []
}
]
}
:query page: The page number in case of a multi-page result set, default is 1
:param organizer: The ``slug`` field of a valid organizer
:param event: The ``slug`` field of the event to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/(id)/
Returns information on one rule, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"list": 12345,
"mode": "placed",
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"all_products": False,
"limit_products": [2, 3],
"limit_variations": [456],
"all_payment_methods": true,
"limit_payment_methods": []
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param id: The ``id`` field of the rule to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to view it.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/
Create a new rule.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 166
{
"list": 12345,
"mode": "placed",
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"all_products": False,
"limit_products": [2, 3],
"limit_variations": [456],
"all_payment_methods": true,
"limit_payment_methods": []
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 1,
"list": 12345,
"mode": "placed",
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"all_products": False,
"limit_products": [2, 3],
"limit_variations": [456],
"all_payment_methods": true,
"limit_payment_methods": []
}
:param organizer: The ``slug`` field of the organizer to create a rule for
:param event: The ``slug`` field of the event to create a rule for
:statuscode 201: no error
:statuscode 400: The rule could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create rules.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/(id)/
Update a rule. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
want to change.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 34
{
"mode": "paid",
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"id": 1,
"list": 12345,
"mode": "placed",
"all_sales_channels": false,
"limit_sales_channels": ["web"],
"all_products": False,
"limit_products": [2, 3],
"limit_variations": [456],
"all_payment_methods": true,
"limit_payment_methods": []
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the rule to modify
:statuscode 200: no error
:statuscode 400: The rule could not be modified due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/auto_checkin_rules/(id)/
Delete a rule.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/auto_checkin_rules/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the rule to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it **or** this rule cannot be deleted since it is currently in use.

View File

@@ -32,6 +32,7 @@ position_count integer Number of ticke
checkin_count integer Number of check-ins performed on this list (read-only).
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
auto_checkin_sales_channels list of strings All items on the check-in list will be automatically marked as checked-in when purchased through any of the listed sales channels.
**Deprecated, will be removed in pretix 2024.10.** Use :ref:`rest-autocheckinrules`: instead.
allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in.
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.

View File

@@ -44,5 +44,6 @@ at :ref:`plugin-docs`.
scheduled_exports
shredders
sendmail_rules
auto_checkin_rules
billing_invoices
billing_var

View File

@@ -1,6 +1,8 @@
Scheduled email rules
=====================
This feature requires the bundled ``pretix.plugins.sendmail`` plugin to be active for the event in order to work properly.
Resource description
--------------------
@@ -48,6 +50,7 @@ send_to string Can be ``"order
or ``"both"``.
date. Otherwise it is relative to the event start date.
===================================== ========================== =======================================================
.. versionchanged:: 2023.7
The ``include_pending`` field has been deprecated.

View File

@@ -62,6 +62,7 @@ INSTALLED_APPS = [
'pretix.plugins.badges',
'pretix.plugins.manualpayment',
'pretix.plugins.returnurl',
'pretix.plugins.autocheckin',
'pretix.plugins.webcheckin',
'django_countries',
'oauth2_provider',

View File

@@ -102,9 +102,9 @@ class CheckinList(LoggedModel):
auto_checkin_sales_channels = models.ManyToManyField(
"SalesChannel",
verbose_name=_('Sales channels to automatically check in'),
help_text=_('All items on this check-in list will be automatically marked as checked-in when purchased through '
'any of the selected sales channels. This option can be useful when tickets sold at the box office '
'are not checked again before entry and should be considered validated directly upon purchase.'),
help_text=_('This option is deprecated and will be removed in the next months. As a replacement, our new plugin '
'"Auto check-in" can be used. When we remove this option, we will automatically migrate your event '
'to use the new plugin.'),
blank=True,
)
rules = models.JSONField(default=dict, blank=True)

View File

@@ -1154,7 +1154,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
)
@receiver(order_placed, dispatch_uid="autocheckin_order_placed")
@receiver(order_placed, dispatch_uid="legacy_autocheckin_order_placed")
def order_placed(sender, **kwargs):
order = kwargs['order']
event = sender
@@ -1171,7 +1171,7 @@ def order_placed(sender, **kwargs):
checkin_created.send(event, checkin=ci)
@receiver(periodic_task, dispatch_uid="autocheckin_exit_all")
@receiver(periodic_task, dispatch_uid="autocheckout_exit_all")
@scopes_disabled()
def process_exit_all(sender, **kwargs):
qs = CheckinList.objects.filter(

View File

@@ -0,0 +1,21 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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/>.
#

View File

@@ -0,0 +1,140 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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 django.core.exceptions import ValidationError
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import serializers, viewsets
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import ItemVariation
from pretix.plugins.autocheckin.models import AutoCheckinRule
from pretix.plugins.sendmail.models import Rule
class AutoCheckinRuleSerializer(I18nAwareModelSerializer):
class Meta:
model = AutoCheckinRule
fields = [
"id",
"list",
"mode",
"all_sales_channels",
"limit_sales_channels",
"all_products",
"limit_products",
"limit_variations",
"all_payment_methods",
"limit_payment_methods",
]
read_only_fields = ["id"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["limit_sales_channels"].child_relation.queryset = self.context[
"event"
].organizer.sales_channels.all()
self.fields["limit_products"].child_relation.queryset = self.context[
"event"
].items.all()
self.fields["limit_variations"].child_relation.queryset = (
ItemVariation.objects.filter(item__event=self.context["event"])
)
self.fields["limit_payment_methods"] = serializers.MultipleChoiceField(
choices=[
(f.identifier, f.verbose_name)
for f in self.context["event"].get_payment_providers().values()
],
required=False,
allow_empty=True,
)
def validate(self, data):
data = super().validate(data)
full_data = (
self.to_internal_value(self.to_representation(self.instance))
if self.instance
else {}
)
full_data.update(data)
if full_data.get("mode") == AutoCheckinRule.MODE_PLACED and not full_data.get(
"all_payment_methods"
):
raise ValidationError("all_payment_methods should be used for mode=placed")
if isinstance(full_data.get("limit_payment_methods"), set):
full_data["limit_payment_methods"] = list(
full_data["limit_payment_methods"]
)
return full_data
def save(self, **kwargs):
return super().save(event=self.context["request"].event)
class RuleViewSet(viewsets.ModelViewSet):
queryset = Rule.objects.none()
serializer_class = AutoCheckinRuleSerializer
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
ordering = ("id",)
ordering_fields = ("id",)
permission = "can_change_event_settings"
def get_queryset(self):
return AutoCheckinRule.objects.filter(event=self.request.event)
def get_serializer_context(self):
return {
**super().get_serializer_context(),
"event": self.request.event,
}
def perform_create(self, serializer):
super().perform_create(serializer)
serializer.instance.log_action(
"pretix.plugins.autocheckin.rule.added",
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
def perform_update(self, serializer):
super().perform_update(serializer)
serializer.instance.log_action(
"pretix.plugins.autocheckin.rule.changed",
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
def perform_destroy(self, instance):
instance.log_action(
"pretix.plugins.autocheckin.rule.deleted",
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
super().perform_destroy(instance)

View File

@@ -0,0 +1,43 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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 django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
from pretix import __version__ as version
class AutoCheckinApp(AppConfig):
name = "pretix.plugins.autocheckin"
verbose_name = _("Automated check-in")
class PretixPluginMeta:
name = _("Automated check-in")
author = _("the pretix team")
version = version
experimental = False
category = "FEATURE"
description = _(
"Automatically check-in specific tickets after they have been sold."
)
def ready(self):
from . import signals # NOQA

View File

@@ -0,0 +1,253 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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/>.
#
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
#
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
#
# This file contains Apache-licensed contributions copyrighted by: Alexey Kislitsin, Daniel, Flavia Bastos, Sanket
# Dasgupta, Sohalt, pajowu
#
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
from django import forms
from django.core.exceptions import ValidationError
from django.urls import reverse
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from django_scopes.forms import (
SafeModelChoiceField, SafeModelMultipleChoiceField,
)
from pretix.base.models import ItemVariation
from pretix.control.forms import SalesChannelCheckboxSelectMultiple
from pretix.control.forms.widgets import Select2
from pretix.plugins.autocheckin.models import AutoCheckinRule
from pretix.base.services.placeholders import FormPlaceholderMixin # noqa
class AutoCheckinRuleForm(forms.ModelForm):
itemvars = forms.MultipleChoiceField(
label=_("Products"),
required=False,
)
limit_payment_methods = forms.MultipleChoiceField(
label=_("Only including usage of payment providers"),
choices=[],
required=False,
widget=forms.RadioSelect,
)
class Meta:
model = AutoCheckinRule
fields = [
"list",
"mode",
"all_sales_channels",
"limit_sales_channels",
"all_products",
"all_payment_methods",
]
field_classes = {
"mode": forms.RadioSelect,
"list": SafeModelChoiceField,
"limit_sales_channels": SafeModelMultipleChoiceField,
}
def __init__(self, *args, **kwargs):
self.event = kwargs.pop("event")
self.instance = kwargs.get("instance", None)
initial = kwargs.get("initial", {})
if self.instance and self.instance.pk and "itemvars" not in initial:
initial["itemvars"] = [
str(i.pk) for i in self.instance.limit_products.all()
] + [
"{}-{}".format(v.item_id, v.pk)
for v in self.instance.limit_variations.all()
]
if (
self.instance
and self.instance.pk
and "limit_payment_methods" not in initial
):
initial["limit_payment_methods"] = self.instance.limit_payment_methods
kwargs["initial"] = initial
super().__init__(*args, **kwargs)
self.fields["limit_sales_channels"].queryset = (
self.event.organizer.sales_channels.all()
)
self.fields["limit_sales_channels"].widget = SalesChannelCheckboxSelectMultiple(
self.event,
attrs={
"data-inverse-dependency": "<[name$=all_sales_channels]",
"class": "scrolling-multiple-choice",
},
choices=self.fields["limit_sales_channels"].widget.choices,
)
choices = []
for item in self.event.items.all():
if len(item.variations.all()) > 0:
allvars = _("All variations")
choices.append(
(
"{}".format(item.pk),
(
f"{item} {allvars}"
if item.active
else mark_safe(
f'<strike class="text-muted">{escape(item)} {allvars}</strike>'
)
),
)
)
else:
choices.append(
(
"{}".format(item.pk),
(
str(item)
if item.active
else mark_safe(
f'<strike class="text-muted">{escape(item)}</strike>'
)
),
)
)
for v in item.variations.all():
choices.append(
(
"{}-{}".format(item.pk, v.pk),
(
"{} {}".format(item, v.value)
if item.active
else mark_safe(
f'<strike class="text-muted">{escape(item)} {escape(v.value)}</strike>'
)
),
)
)
self.fields["itemvars"].widget = forms.CheckboxSelectMultiple(
attrs={
"data-inverse-dependency": "<[name$=all_products]",
"class": "scrolling-multiple-choice",
},
)
self.fields["itemvars"].choices = choices
self.fields["list"].queryset = self.event.checkin_lists.all()
self.fields["list"].widget = Select2(
attrs={
"data-model-select2": "generic",
"data-select2-url": reverse(
"control:event.orders.checkinlists.select2",
kwargs={
"event": self.event.slug,
"organizer": self.event.organizer.slug,
},
),
"data-placeholder": _("Check-in list"),
}
)
self.fields["list"].widget.choices = self.fields["list"].choices
self.fields["list"].label = _("Check-in list")
self.fields["list"].widget.choices = self.fields["list"].choices
self.fields["limit_payment_methods"].choices += [
(p.identifier, p.verbose_name)
for p in self.event.get_payment_providers().values()
]
self.fields["limit_payment_methods"].widget = forms.CheckboxSelectMultiple(
attrs={
"data-inverse-dependency": "<[name$=all_payment_methods]",
"class": "scrolling-multiple-choice",
},
choices=self.fields["limit_payment_methods"].choices,
)
def save(self, *args, **kwargs):
creating = not self.instance.pk
self.instance.limit_payment_methods = (
self.cleaned_data.get("limit_payment_methods") or []
)
inst = super().save(*args, **kwargs)
selected_items = set(
list(
self.event.items.filter(
id__in=[i for i in self.cleaned_data["itemvars"] if "-" not in i]
)
)
)
selected_variations = list(
ItemVariation.objects.filter(
item__event=self.event,
id__in=[
i.split("-")[1] for i in self.cleaned_data["itemvars"] if "-" in i
],
)
)
current_items = [] if creating else self.instance.limit_products.all()
current_variations = [] if creating else self.instance.limit_variations.all()
self.instance.limit_products.remove(
*[i for i in current_items if i not in selected_items]
)
self.instance.limit_products.add(
*[i for i in selected_items if i not in current_items]
)
self.instance.limit_variations.remove(
*[i for i in current_variations if i not in selected_variations]
)
self.instance.limit_variations.add(
*[i for i in selected_variations if i not in current_variations]
)
return inst
def clean(self):
d = super().clean()
if d["mode"] == AutoCheckinRule.MODE_PLACED and not d["all_payment_methods"]:
raise ValidationError(
{
"mode": _(
"When restricting by payment method, the rule should run after the payment was received."
)
}
)
return d

View File

@@ -0,0 +1,61 @@
# Generated by Django 4.2.14 on 2024-07-24 08:36
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
("pretixbase", "0269_order_api_meta"),
]
operations = [
migrations.CreateModel(
name="AutoCheckinRule",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("mode", models.CharField(default="placed", max_length=100)),
("all_sales_channels", models.BooleanField(default=True)),
("all_products", models.BooleanField(default=True)),
("all_payment_methods", models.BooleanField(default=True)),
(
"limit_payment_methods",
pretix.base.models.fields.MultiStringField(null=True),
),
(
"event",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="pretixbase.event",
),
),
("limit_products", models.ManyToManyField(to="pretixbase.item")),
(
"limit_sales_channels",
models.ManyToManyField(to="pretixbase.saleschannel"),
),
(
"limit_variations",
models.ManyToManyField(to="pretixbase.itemvariation"),
),
(
"list",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="pretixbase.checkinlist",
),
),
],
),
]

View File

@@ -0,0 +1,85 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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 django.db import models
from django.utils.translation import gettext_lazy as _
from pretix.base.models import (
CheckinList, Event, Item, ItemVariation, LoggedModel, SalesChannel,
)
from pretix.base.models.fields import MultiStringField
class AutoCheckinRule(LoggedModel):
MODE_PLACED = "placed"
MODE_PAID = "paid"
MODE_CHOICES = (
(MODE_PLACED, _("After order was placed")),
(MODE_PAID, _("After order was paid")),
)
event = models.ForeignKey(Event, on_delete=models.CASCADE)
list = models.ForeignKey(
CheckinList,
on_delete=models.CASCADE,
null=True,
blank=True,
verbose_name=_("Check-in list"),
help_text=_(
"If you keep this empty, all lists that match the purchased product will be used."
),
)
mode = models.CharField(
max_length=100,
choices=MODE_CHOICES,
default=MODE_PLACED,
)
all_sales_channels = models.BooleanField(
verbose_name=_("All sales channels"),
default=True,
)
limit_sales_channels = models.ManyToManyField(
SalesChannel,
verbose_name=_("Sales channels"),
blank=True,
)
all_products = models.BooleanField(
verbose_name=_("All products and variations"),
default=True,
)
limit_products = models.ManyToManyField(Item, verbose_name=_("Products"), blank=True)
limit_variations = models.ManyToManyField(
ItemVariation, blank=True, verbose_name=_("Variations")
)
all_payment_methods = models.BooleanField(
verbose_name=_("All payment methods"),
default=True,
)
limit_payment_methods = MultiStringField(
verbose_name=_("Only including usage of payment providers"),
null=True,
blank=True,
)

View File

@@ -0,0 +1,194 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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/>.
#
import copy
from django.db.models import Q
from django.dispatch import receiver
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from pretix.base.models import Checkin, OrderPayment
from pretix.base.signals import (
checkin_created, event_copy_data, item_copy_data, logentry_display,
order_paid, order_placed,
)
from pretix.control.signals import nav_event
from pretix.plugins.autocheckin.models import AutoCheckinRule
@receiver(nav_event, dispatch_uid="autocheckin_nav_event")
def nav_event_receiver(sender, request, **kwargs):
url = request.resolver_match
if not request.user.has_event_permission(
request.organizer, request.event, "can_change_event_settings", request=request
):
return []
return [
{
"label": _("Auto check-in"),
"url": reverse(
"plugins:autocheckin:index",
kwargs={
"event": request.event.slug,
"organizer": request.organizer.slug,
},
),
"parent": reverse(
"control:event.orders.checkinlists",
kwargs={
"event": request.event.slug,
"organizer": request.event.organizer.slug,
},
),
"active": url.namespace == "plugins:autocheckin",
}
]
@receiver(signal=logentry_display)
def logentry_display_receiver(sender, logentry, **kwargs):
plains = {
"pretix.plugins.autocheckin.rule.added": _("An auto check-in rule was created"),
"pretix.plugins.autocheckin.rule.changed": _(
"An auto check-in rule was updated"
),
"pretix.plugins.autocheckin.rule.deleted": _(
"An auto check-in rule was deleted"
),
}
if logentry.action_type in plains:
return plains[logentry.action_type]
@receiver(item_copy_data, dispatch_uid="autocheckin_item_copy")
def item_copy_data_receiver(sender, source, target, **kwargs):
for acr in AutoCheckinRule.objects.filter(limit_products=source):
acr.limit_products.add(target)
@receiver(signal=event_copy_data, dispatch_uid="autocheckin_copy_data")
def event_copy_data_receiver(
sender, other, item_map, variation_map, checkin_list_map, **kwargs
):
for acr in other.autocheckinrule_set.all():
if acr.list and acr.list.subevent:
continue # Impossible to copy
oldacr = acr
acr = copy.copy(acr)
acr.pk = None
acr.event = sender
if acr.list_id:
acr.list = checkin_list_map[acr.list_id]
acr.save()
if not acr.all_sales_channels:
acr.limit_sales_channels.set(
sender.organizer.sales_channels.filter(
identifier__in=oldacr.limit_sales_channels.values_list(
"identifier", flat=True
)
)
)
if not acr.all_products:
acr.limit_products.set([item_map[o.pk] for o in oldacr.limit_products.all()])
acr.limit_variations.set(
[variation_map[o.pk] for o in oldacr.limit_variations.all()]
)
def perform_auto_checkin(sender, order, mode, payment_methods):
positions = list(order.positions.all())
payment_q = Q(all_payment_methods=True)
for p in payment_methods:
payment_q = payment_q | Q(limit_payment_methods__contains=p)
rules = list(
sender.autocheckinrule_set.filter(
Q(all_sales_channels=True) | Q(limit_sales_channels=order.sales_channel_id),
Q(all_products=True)
| Q(limit_products__in=[op.item_id for op in positions])
| Q(limit_variations__in=[op.variation_id for op in positions]),
payment_q,
mode=mode,
)
.distinct()
.select_related("list")
)
if any(r.list is None for r in rules):
all_lists = sender.checkin_lists.filter(
Q(subevent__isnull=True)
| Q(subevent__in=[op.subevent_id for op in positions])
).prefetch_related("limit_products")
else:
all_lists = []
for r in rules:
if r.list is not None:
lists = [r.list]
else:
lists = all_lists
for cl in lists:
for op in positions:
if not cl.all_products and op.item_id not in {
i.pk for i in cl.limit_products.all()
}:
continue
if cl.subevent_id and cl.subevent_id != op.subevent_id:
continue
ci, created = Checkin.objects.get_or_create(
position=op,
list=cl,
auto_checked_in=True,
type=Checkin.TYPE_ENTRY,
)
if created:
checkin_created.send(sender, checkin=ci)
@receiver(order_placed, dispatch_uid="autocheckin_order_placed")
def order_placed_receiver(sender, order, **kwargs):
mode = AutoCheckinRule.MODE_PLACED
payment_methods = set()
perform_auto_checkin(sender, order, mode, payment_methods)
@receiver(order_paid, dispatch_uid="autocheckin_order_paid")
def order_paid_receiver(sender, order, **kwargs):
mode = AutoCheckinRule.MODE_PAID
payment_methods = {
p.provider
for p in order.payments.filter(
state__in=[
OrderPayment.PAYMENT_STATE_CONFIRMED,
OrderPayment.PAYMENT_STATE_REFUNDED,
]
)
}
perform_auto_checkin(sender, order, mode, payment_methods)

View File

@@ -0,0 +1,34 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Create auto check-in rule" %}{% endblock %}
{% block content %}
<h1>{% trans "Create auto check-in rule" %}</h1>
{% block inner %}
<form class="form-horizontal" method="post" action="" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "Auto check-in" %}</legend>
{% bootstrap_field form.mode layout='control' %}
{% bootstrap_field form.list layout='control' %}
</fieldset>
<fieldset>
<legend>{% trans "Conditions" %}</legend>
{% bootstrap_field form.all_sales_channels layout='control' %}
{% bootstrap_field form.limit_sales_channels layout='control' %}
{% bootstrap_field form.all_products layout='control' %}
{% bootstrap_field form.itemvars layout='control' %}
{% bootstrap_field form.all_payment_methods layout='control' %}
{% bootstrap_field form.limit_payment_methods layout='control' %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}
{% endblock %}

View File

@@ -0,0 +1,19 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Delete auto-checkin rule" %}{% endblock %}
{% block content %}
<h1>{% trans "Delete auto-checkin rule" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>{% blocktrans %}Are you sure you want to delete the auto check-in rule?{% endblocktrans %}</p>
<div class="form-group submit-group">
<a class="btn btn-default btn-cancel" href="{% url "plugins:autocheckin:index" organizer=request.organizer.slug event=request.event.slug %}">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% trans "Delete" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,34 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Auto check-in rule" %}{% endblock %}
{% block content %}
<h1>{% trans "Auto check-in rule" %}</h1>
{% block inner %}
<form class="form-horizontal" method="post" action="" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "Auto check-in" %}</legend>
{% bootstrap_field form.mode layout='control' %}
{% bootstrap_field form.list layout='control' %}
</fieldset>
<fieldset>
<legend>{% trans "Conditions" %}</legend>
{% bootstrap_field form.all_sales_channels layout='control' %}
{% bootstrap_field form.limit_sales_channels layout='control' %}
{% bootstrap_field form.all_products layout='control' %}
{% bootstrap_field form.itemvars layout='control' %}
{% bootstrap_field form.all_payment_methods layout='control' %}
{% bootstrap_field form.limit_payment_methods layout='control' %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}
{% endblock %}

View File

@@ -0,0 +1,100 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load money %}
{% load static %}
{% block title %}{% trans "Auto check-in rules" %}{% endblock %}
{% block content %}
<h1>{% trans "Auto check-in rules" %}</h1>
{% if rules|length == 0 %}
<div class="empty-collection">
<p>
{% blocktrans trimmed %}
You haven't created any rules yet.
{% endblocktrans %}
</p>
<a href="{% url "plugins:autocheckin:add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new check-in rule" %}
</a>
</div>
{% else %}
<p>
<a href="{% url "plugins:autocheckin:add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new check-in rule" %}
</a>
</p>
<div class="table-responsive">
<table class="table table-hover table-quotas">
<thead>
<tr>
<th>{% trans "Check-in list" %}</th>
<th>{% trans "Sales channels" %}</th>
<th>{% trans "Products" %}</th>
<th>{% trans "Payment methods" %}</th>
<th class="action-col-2"></th>
</tr>
</thead>
<tbody>
{% for r in rules %}
<tr>
<td>
{% if r.list %}
{{ r.list }}
{% else %}
<em>{% trans "All" %}</em>
{% endif %}
</td>
<td>
{% for c in sales_channels %}
{% if r.all_sales_channels or c in r.limit_sales_channels.all %}
{% if "." in c.icon %}
<img src="{% static c.icon %}" class="fa-like-image"
data-toggle="tooltip" title="{{ c.label }}">
{% else %}
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
data-toggle="tooltip" title="{{ c.label }}"></span>
{% endif %}
{% else %}
{% endif %}
{% endfor %}
</td>
<td>
{% if r.all_products %}
<em>{% trans "All" %}</em>
{% else %}
<ul>
{% for i in r.limit_products.all %}
<li>{{ i }}</li>
{% endfor %}
{% for v in r.limit_variations.all %}
<li>{{ v.item }} {{ v.value }}</li>
{% endfor %}
</ul>
{% endif %}
</td>
<td>
{% if r.all_payment_methods %}
<em>{% trans "All" %}</em>
{% else %}
<ul>
{% for p in r.pprovs %}
<li>{{ p.verbose_name }}</li>
{% endfor %}
</ul>
{% endif %}
</td>
<td class="text-right flip">
{% if "can_change_event_settings" in request.eventpermset %}
<a href="{% url "plugins:autocheckin:edit" organizer=request.event.organizer.slug event=request.event.slug rule=r.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "plugins:autocheckin:add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ r.id }}"
class="btn btn-default btn-sm" title="{% trans "Clone" %}" data-toggle="tooltip"><i class="fa fa-copy"></i></a>
<a href="{% url "plugins:autocheckin:delete" organizer=request.event.organizer.slug event=request.event.slug rule=r.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% include "pretixcontrol/pagination.html" %}
{% endblock %}

View File

@@ -0,0 +1,51 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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 django.urls import re_path
from pretix.api.urls import event_router
from . import views
from .api import RuleViewSet
urlpatterns = [
re_path(
r"^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/autocheckin/$",
views.IndexView.as_view(),
name="index",
),
re_path(
r"^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/autocheckin/add$",
views.RuleAddView.as_view(),
name="add",
),
re_path(
r"^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/autocheckin/(?P<rule>\d+)/delete$",
views.RuleDeleteView.as_view(),
name="delete",
),
re_path(
r"^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/autocheckin/(?P<rule>\d+)/$",
views.RuleEditView.as_view(),
name="edit",
),
]
event_router.register(r"auto_checkin_rules", RuleViewSet, basename="autocheckinrules")

View File

@@ -0,0 +1,210 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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 django.contrib import messages
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django.views.generic import DeleteView, ListView
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views import CreateView, UpdateView
from pretix.helpers.models import modelcopy
from pretix.plugins.autocheckin.forms import AutoCheckinRuleForm
from pretix.plugins.autocheckin.models import AutoCheckinRule
class IndexView(EventPermissionRequiredMixin, ListView):
permission = "can_change_event_settings"
template_name = "pretixplugins/autocheckin/index.html"
paginate_by = 50
context_object_name = "rules"
def get_queryset(self):
return (
self.request.event.autocheckinrule_set.select_related(
"list",
)
.prefetch_related(
"limit_sales_channels",
"limit_products",
"limit_variations",
"limit_variations__item",
)
.order_by(
"list__name",
"list_id",
"pk",
)
)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["sales_channels"] = self.request.organizer.sales_channels.all()
pprovs = self.request.event.get_payment_providers()
for r in ctx["rules"]:
r.pprovs = [pprovs[p] for p in r.limit_payment_methods if p in pprovs]
return ctx
class RuleAddView(EventPermissionRequiredMixin, CreateView):
template_name = "pretixplugins/autocheckin/add.html"
permission = "can_change_event_settings"
form_class = AutoCheckinRuleForm
model = AutoCheckinRule
@cached_property
def copy_from(self):
if self.request.GET.get("copy_from") and not getattr(self, "object", None):
try:
return AutoCheckinRule.objects.get(
pk=self.request.GET.get("copy_from"), event=self.request.event
)
except AutoCheckinRule.DoesNotExist:
pass
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["event"] = self.request.event
if self.copy_from:
i = modelcopy(self.copy_from)
i.pk = None
kwargs["instance"] = i
kwargs.setdefault("initial", {})
kwargs["initial"]["itemvars"] = [
str(i.pk) for i in self.copy_from.limit_products.all()
] + [
"{}-{}".format(v.item_id, v.pk)
for v in self.copy_from.limit_variations.all()
]
kwargs["initial"][
"limit_payment_methods"
] = self.copy_from.limit_payment_methods
kwargs["initial"][
"limit_sales_channels"
] = self.copy_from.limit_sales_channels.all()
return kwargs
def form_invalid(self, form):
messages.error(
self.request, _("We could not save your changes. See below for details.")
)
return super().form_invalid(form)
def form_valid(self, form):
self.output = {}
messages.success(self.request, _("Your rule has been created."))
form.instance.event = self.request.event
with transaction.atomic():
self.object = form.save()
form.instance.log_action(
"pretix.plugins.autocheckin.rule.added",
user=self.request.user,
data=dict(form.cleaned_data),
)
return redirect(
"plugins:autocheckin:edit",
event=self.request.event.slug,
organizer=self.request.event.organizer.slug,
rule=self.object.pk,
)
class RuleEditView(EventPermissionRequiredMixin, UpdateView):
model = AutoCheckinRule
form_class = AutoCheckinRuleForm
template_name = "pretixplugins/autocheckin/edit.html"
permission = "can_change_event_settings"
def get_object(self, queryset=None) -> AutoCheckinRule:
return get_object_or_404(
AutoCheckinRule.objects.all(),
event=self.request.event,
id=self.kwargs["rule"],
)
def get_success_url(self):
return reverse(
"plugins:autocheckin:edit",
kwargs={
"organizer": self.request.event.organizer.slug,
"event": self.request.event.slug,
"rule": self.object.pk,
},
)
@transaction.atomic()
def form_valid(self, form):
messages.success(self.request, _("Your changes have been saved."))
form.instance.log_action(
"pretix.plugins.autocheckin.rule.changed",
user=self.request.user,
data=dict(form.cleaned_data),
)
return super().form_valid(form)
def form_invalid(self, form):
messages.error(
self.request, _("We could not save your changes. See below for details.")
)
return super().form_invalid(form)
class RuleDeleteView(EventPermissionRequiredMixin, DeleteView):
model = AutoCheckinRule
permission = "can_change_event_settings"
template_name = "pretixplugins/autocheckin/delete.html"
context_object_name = "rule"
def get_success_url(self):
return reverse(
"plugins:autocheckin:index",
kwargs={
"organizer": self.request.event.organizer.slug,
"event": self.request.event.slug,
},
)
def get_object(self, queryset=None) -> AutoCheckinRule:
return get_object_or_404(
AutoCheckinRule, event=self.request.event, id=self.kwargs["rule"]
)
@transaction.atomic
def form_valid(self, request, *args, **kwargs):
self.object = self.get_object()
success_url = self.get_success_url()
self.request.event.log_action(
"pretix.plugins.autocheckin.rule.deleted", user=self.request.user, data={}
)
self.object.delete()
messages.success(self.request, _("The selected rule has been deleted."))
return redirect(success_url)

View File

@@ -212,7 +212,7 @@ if config.getboolean('pretix', 'trust_x_forwarded_proto', fallback=False):
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
PRETIX_PLUGINS_DEFAULT = config.get('pretix', 'plugins_default',
fallback='pretix.plugins.sendmail,pretix.plugins.statistics,pretix.plugins.checkinlists,pretix.plugins.autocheckin')
fallback='pretix.plugins.sendmail,pretix.plugins.statistics,pretix.plugins.checkinlists')
PRETIX_PLUGINS_EXCLUDE = config.get('pretix', 'plugins_exclude', fallback='').split(',')
PRETIX_PLUGINS_SHOW_META = config.getboolean('pretix', 'plugins_show_meta', fallback=True)

View File

@@ -0,0 +1,21 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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/>.
#

View File

@@ -0,0 +1,96 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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 datetime, timezone
import pytest
from django_scopes import scopes_disabled
from rest_framework.test import APIClient
from pretix.base.models import Event, Organizer, Team
@pytest.fixture
@scopes_disabled()
def organizer():
return Organizer.objects.create(name="Dummy", slug="dummy")
@pytest.fixture
@scopes_disabled()
def event(organizer):
e = Event.objects.create(
organizer=organizer,
name="Dummy",
slug="dummy",
date_from=datetime(2017, 12, 27, 10, 0, 0, tzinfo=timezone.utc),
plugins="pretix.plugins.banktransfer,pretix.plugins.ticketoutputpdf,pretix.plugins.autocheckin",
is_public=True,
)
e.settings.timezone = "Europe/Berlin"
return e
@pytest.fixture
@scopes_disabled()
def item(event):
return event.items.create(name="foo", default_price=3)
@pytest.fixture
@scopes_disabled()
def checkin_list(event):
return event.checkin_lists.create(name="foo")
@pytest.fixture
@scopes_disabled()
def team(organizer):
return Team.objects.create(
organizer=organizer,
name="Test-Team",
all_events=True,
can_change_teams=True,
can_manage_gift_cards=True,
can_change_items=True,
can_create_events=True,
can_change_event_settings=True,
can_change_vouchers=True,
can_view_vouchers=True,
can_view_orders=True,
can_change_orders=True,
can_manage_customers=True,
can_manage_reusable_media=True,
can_change_organizer_settings=True,
)
@pytest.fixture
def client():
return APIClient()
@pytest.fixture
@scopes_disabled()
def token_client(client, team):
t = team.tokens.create(name="Foo")
client.credentials(HTTP_AUTHORIZATION="Token " + t.token)
return client

View File

@@ -0,0 +1,155 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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/>.
#
import copy
import pytest
from django_scopes import scopes_disabled
@pytest.fixture
def acr(event, item):
acr = event.autocheckinrule_set.create(
all_products=False,
)
acr.limit_products.add(item)
return acr
RES_RULE = {
"list": None,
"mode": "placed",
"all_sales_channels": True,
"limit_sales_channels": [],
"all_products": False,
"limit_products": [],
"limit_variations": [],
"all_payment_methods": True,
"limit_payment_methods": set(),
}
@pytest.mark.django_db
def test_api_list(event, acr, item, token_client):
res = copy.copy(RES_RULE)
res["id"] = acr.pk
res["limit_products"] = [item.pk]
r = token_client.get(
"/api/v1/organizers/{}/events/{}/auto_checkin_rules/".format(
event.organizer.slug, event.slug
)
).data
assert r["results"] == [res]
@pytest.mark.django_db
def test_api_detail(event, acr, item, token_client):
res = copy.copy(RES_RULE)
res["id"] = acr.pk
res["limit_products"] = [item.pk]
r = token_client.get(
"/api/v1/organizers/{}/events/{}/auto_checkin_rules/{}/".format(
event.organizer.slug, event.slug, acr.pk
)
).data
assert r == res
@pytest.mark.django_db
def test_api_create(event, acr, item, token_client):
resp = token_client.post(
"/api/v1/organizers/{}/events/{}/auto_checkin_rules/".format(
event.slug, event.slug
),
{
"all_products": False,
"limit_products": [item.pk],
},
format="json",
)
assert resp.status_code == 201
with scopes_disabled():
acr = event.autocheckinrule_set.get(pk=resp.data["id"])
assert list(acr.limit_products.all()) == [item]
@pytest.mark.django_db
def test_api_create_validate_pprov(event, acr, item, token_client):
resp = token_client.post(
"/api/v1/organizers/{}/events/{}/auto_checkin_rules/".format(
event.slug, event.slug
),
{
"mode": "placed",
"all_payment_methods": False,
"limit_payment_methods": ["manual"],
},
format="json",
)
assert resp.status_code == 400
assert resp.data == {
"non_field_errors": ["all_payment_methods should be used for mode=placed"]
}
resp = token_client.post(
"/api/v1/organizers/{}/events/{}/auto_checkin_rules/".format(
event.slug, event.slug
),
{
"mode": "paid",
"all_payment_methods": False,
"limit_payment_methods": ["unknown"],
},
format="json",
)
assert resp.status_code == 400
assert resp.data == {"limit_payment_methods": ['"unknown" is not a valid choice.']}
@pytest.mark.django_db
def test_api_update(event, acr, item, token_client):
resp = token_client.patch(
"/api/v1/organizers/{}/events/{}/auto_checkin_rules/{}/".format(
event.slug, event.slug, acr.pk
),
{
"mode": "paid",
"all_payment_methods": False,
"limit_payment_methods": ["manual"],
},
format="json",
)
assert resp.status_code == 200
acr.refresh_from_db()
assert acr.all_payment_methods is False
assert acr.limit_payment_methods == ["manual"]
@pytest.mark.django_db
def test_api_delete(event, acr, item, token_client):
resp = token_client.delete(
"/api/v1/organizers/{}/events/{}/auto_checkin_rules/{}/".format(
event.slug, event.slug, acr.pk
),
)
assert resp.status_code == 204
assert not event.autocheckinrule_set.exists()

View File

@@ -0,0 +1,310 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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/>.
#
import copy
from datetime import timedelta
import pytest
from django.utils.timezone import now
from django_scopes import scopes_disabled
from tests.api.test_order_create import ORDER_CREATE_PAYLOAD
from pretix.base.models import Order, OrderPayment
from pretix.base.signals import order_paid, order_placed
from pretix.plugins.autocheckin.models import AutoCheckinRule
@pytest.fixture
@scopes_disabled()
def order(organizer, event, item):
order = Order.objects.create(
event=event,
status=Order.STATUS_PENDING,
expires=now() + timedelta(days=3),
sales_channel=organizer.sales_channels.get(identifier="web"),
total=4,
)
order.positions.create(order=order, item=item, price=2)
return order
@pytest.mark.django_db
@scopes_disabled()
def test_sales_channel_all(event, item, order, checkin_list):
event.autocheckinrule_set.create(
list=checkin_list,
mode=AutoCheckinRule.MODE_PLACED,
all_sales_channels=True,
)
order_placed.send(event, order=order)
assert order.positions.first().checkins.exists()
@pytest.mark.django_db
@scopes_disabled()
def test_sales_channel_limit(event, item, order, checkin_list):
acr = event.autocheckinrule_set.create(
list=checkin_list,
mode=AutoCheckinRule.MODE_PLACED,
all_sales_channels=False,
)
order_placed.send(event, order=order)
assert not order.positions.first().checkins.exists()
acr.limit_sales_channels.add(order.sales_channel)
order_placed.send(event, order=order)
assert order.positions.first().checkins.exists()
@pytest.mark.django_db
@scopes_disabled()
def test_items_all(event, item, order, checkin_list):
event.autocheckinrule_set.create(
list=checkin_list,
mode=AutoCheckinRule.MODE_PLACED,
all_products=True,
)
order_placed.send(event, order=order)
assert order.positions.first().checkins.exists()
@pytest.mark.django_db
@scopes_disabled()
def test_items_limit(event, item, order, checkin_list):
acr = event.autocheckinrule_set.create(
list=checkin_list,
mode=AutoCheckinRule.MODE_PLACED,
all_products=False,
)
order_placed.send(event, order=order)
assert not order.positions.first().checkins.exists()
acr.limit_products.add(item)
order_placed.send(event, order=order)
assert order.positions.first().checkins.exists()
@pytest.mark.django_db
@scopes_disabled()
def test_variations_limit(event, item, order, checkin_list):
var = item.variations.create(value="V1")
op = order.positions.first()
op.variation = var
op.save()
acr = event.autocheckinrule_set.create(
list=checkin_list,
mode=AutoCheckinRule.MODE_PLACED,
all_products=False,
)
order_placed.send(event, order=order)
assert not order.positions.first().checkins.exists()
acr.limit_variations.add(var)
order_placed.send(event, order=order)
assert order.positions.first().checkins.exists()
order.positions.first().checkins.all().delete()
acr.limit_products.add(item)
acr.limit_variations.clear()
order_placed.send(event, order=order)
assert order.positions.first().checkins.exists()
@pytest.mark.django_db
@scopes_disabled()
def test_mode_placed(event, item, order, checkin_list):
event.autocheckinrule_set.create(
list=checkin_list,
mode=AutoCheckinRule.MODE_PLACED,
)
order_paid.send(event, order=order)
assert not order.positions.first().checkins.exists()
order_placed.send(event, order=order)
assert order.positions.first().checkins.exists()
@pytest.mark.django_db
@scopes_disabled()
def test_mode_paid(event, item, order, checkin_list):
event.autocheckinrule_set.create(
list=checkin_list,
mode=AutoCheckinRule.MODE_PAID,
)
order_placed.send(event, order=order)
assert not order.positions.first().checkins.exists()
order_paid.send(event, order=order)
assert order.positions.first().checkins.exists()
@pytest.mark.django_db
@scopes_disabled()
def test_payment_provider_limit(event, item, order, checkin_list):
event.autocheckinrule_set.create(
list=checkin_list,
mode=AutoCheckinRule.MODE_PAID,
all_payment_methods=False,
limit_payment_methods=["manual"],
)
p = order.payments.create(
amount=order.total,
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
provider="banktransfer",
)
order_paid.send(event, order=order)
assert not order.positions.first().checkins.exists()
p.provider = "manual"
p.save()
order_paid.send(event, order=order)
assert order.positions.first().checkins.exists()
@pytest.mark.django_db
@scopes_disabled()
def test_idempodency(event, item, order, checkin_list):
event.autocheckinrule_set.create(
list=checkin_list,
mode=AutoCheckinRule.MODE_PAID,
)
order_paid.send(event, order=order)
assert order.positions.first().checkins.count() == 1
order_paid.send(event, order=order)
assert order.positions.first().checkins.count() == 1
@pytest.mark.django_db
@scopes_disabled()
def test_multiple_rules_same_list(event, item, order, checkin_list):
event.autocheckinrule_set.create(
list=checkin_list,
mode=AutoCheckinRule.MODE_PAID,
)
event.autocheckinrule_set.create(
list=checkin_list,
mode=AutoCheckinRule.MODE_PAID,
)
order_paid.send(event, order=order)
assert order.positions.first().checkins.count() == 1
@pytest.mark.django_db
@scopes_disabled()
def test_multiple_rules_different_lists(event, item, order, checkin_list):
cl2 = event.checkin_lists.create(name="bar")
event.autocheckinrule_set.create(
list=checkin_list,
mode=AutoCheckinRule.MODE_PAID,
)
event.autocheckinrule_set.create(
list=cl2,
mode=AutoCheckinRule.MODE_PAID,
)
order_paid.send(event, order=order)
assert order.positions.first().checkins.count() == 2
@pytest.mark.django_db
@scopes_disabled()
def test_autodetect_lists(event, item, order, checkin_list):
cl2 = event.checkin_lists.create(name="bar", all_products=False)
cl2.limit_products.add(item)
event.checkin_lists.create(name="baz", all_products=False)
event.autocheckinrule_set.create(
mode=AutoCheckinRule.MODE_PAID,
)
order_paid.send(event, order=order)
assert {c.list_id for c in order.positions.first().checkins.all()} == {
checkin_list.pk,
cl2.pk,
}
@pytest.mark.django_db
@scopes_disabled()
def test_order_create_via_api_placed(
token_client, organizer, event, item, checkin_list
):
acr = event.autocheckinrule_set.create(
list=checkin_list,
mode=AutoCheckinRule.MODE_PLACED,
all_sales_channels=False,
)
acr.limit_sales_channels.add(organizer.sales_channels.get(identifier="web"))
q = event.quotas.create(name="Foo", size=None)
q.items.add(item)
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res["positions"][0]["item"] = item.pk
res["positions"][0]["answers"] = []
resp = token_client.post(
"/api/v1/organizers/{}/events/{}/orders/".format(organizer.slug, event.slug),
format="json",
data=res,
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data["code"])
assert o.positions.first().checkins.first().auto_checked_in
@pytest.mark.django_db
@scopes_disabled()
def test_order_create_via_api_paid(token_client, organizer, event, item, checkin_list):
event.autocheckinrule_set.create(
list=checkin_list,
mode=AutoCheckinRule.MODE_PAID,
all_payment_methods=False,
limit_payment_methods=["manual"],
)
q = event.quotas.create(name="Foo", size=None)
q.items.add(item)
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res["positions"][0]["item"] = item.pk
res["positions"][0]["answers"] = []
res["status"] = "p"
res["payment_provider"] = "manual"
resp = token_client.post(
"/api/v1/organizers/{}/events/{}/orders/".format(organizer.slug, event.slug),
format="json",
data=res,
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data["code"])
assert o.positions.first().checkins.first().auto_checked_in

View File

@@ -0,0 +1,185 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# 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/>.
#
import datetime
from django_scopes import scopes_disabled
from tests.base import SoupTest, extract_form_fields
from pretix.base.models import Event, Item, Organizer, Team, User
from pretix.plugins.autocheckin.models import AutoCheckinRule
class AutoCheckinFormTest(SoupTest):
def setUp(self):
super().setUp()
self.user = User.objects.create_user("dummy@dummy.dummy", "dummy")
self.orga1 = Organizer.objects.create(name="CCC", slug="ccc")
self.orga2 = Organizer.objects.create(name="MRM", slug="mrm")
self.event1 = Event.objects.create(
organizer=self.orga1,
name="30C3",
slug="30c3",
plugins="pretix.plugins.autocheckin",
date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc),
)
self.item1 = Item.objects.create(
event=self.event1, name="Standard", default_price=0, position=1
)
t = Team.objects.create(
organizer=self.orga1,
can_change_event_settings=True,
can_view_orders=True,
can_change_items=True,
all_events=True,
can_create_events=True,
can_change_orders=True,
can_change_vouchers=True,
)
t.members.add(self.user)
t = Team.objects.create(
organizer=self.orga2,
can_change_event_settings=True,
can_view_orders=True,
can_change_items=True,
all_events=True,
can_create_events=True,
can_change_orders=True,
can_change_vouchers=True,
)
t.members.add(self.user)
self.client.login(email="dummy@dummy.dummy", password="dummy")
def test_create(self):
doc = self.get_doc(
"/control/event/%s/%s/autocheckin/add" % (self.orga1.slug, self.event1.slug)
)
form_data = extract_form_fields(doc.select(".container-fluid form")[0])
doc = self.post_doc(
"/control/event/%s/%s/autocheckin/add"
% (self.orga1.slug, self.event1.slug),
form_data,
)
assert doc.select(".alert-success")
assert self.event1.autocheckinrule_set.exists()
def test_delete(self):
acr = self.event1.autocheckinrule_set.create(
mode=AutoCheckinRule.MODE_PAID,
all_payment_methods=False,
limit_payment_methods=["manual"],
)
doc = self.get_doc(
"/control/event/%s/%s/autocheckin/%s/delete"
% (self.orga1.slug, self.event1.slug, acr.id)
)
form_data = extract_form_fields(doc.select(".container-fluid form")[0])
doc = self.post_doc(
"/control/event/%s/%s/autocheckin/%s/delete"
% (self.orga1.slug, self.event1.slug, acr.id),
form_data,
)
assert doc.select(".alert-success")
with scopes_disabled():
assert self.event1.autocheckinrule_set.count() == 0
def test_item_copy(self):
with scopes_disabled():
acr = self.event1.autocheckinrule_set.create(
mode=AutoCheckinRule.MODE_PAID, all_products=False
)
acr.limit_products.add(self.item1)
self.client.post(
"/control/event/%s/%s/items/add" % (self.orga1.slug, self.event1.slug),
{
"name_0": "Intermediate",
"default_price": "23.00",
"tax_rate": "19.00",
"copy_from": str(self.item1.pk),
"has_variations": "1",
},
)
with scopes_disabled():
acr.refresh_from_db()
i_new = Item.objects.get(name__icontains="Intermediate")
assert i_new in acr.limit_products.all()
def test_copy_event(self):
with scopes_disabled():
acr = self.event1.autocheckinrule_set.create(
list=self.event1.checkin_lists.create(name="Test"),
mode=AutoCheckinRule.MODE_PAID,
all_products=False,
all_sales_channels=False,
)
acr.limit_products.add(self.item1)
acr.limit_sales_channels.add(
self.orga1.sales_channels.get(identifier="web")
)
self.post_doc(
"/control/events/add",
{
"event_wizard-current_step": "foundation",
"event_wizard-prefix": "event_wizard",
"foundation-organizer": self.orga2.pk,
"foundation-locales": ("en",),
},
)
self.post_doc(
"/control/events/add",
{
"event_wizard-current_step": "basics",
"event_wizard-prefix": "event_wizard",
"basics-name_0": "33C3",
"basics-slug": "33c3",
"basics-date_from_0": "2016-12-27",
"basics-date_from_1": "10:00:00",
"basics-date_to_0": "2016-12-30",
"basics-date_to_1": "19:00:00",
"basics-location_0": "Hamburg",
"basics-currency": "EUR",
"basics-tax_rate": "19.00",
"basics-locale": "en",
"basics-timezone": "Europe/Berlin",
},
)
self.post_doc(
"/control/events/add",
{
"event_wizard-current_step": "copy",
"event_wizard-prefix": "event_wizard",
"copy-copy_from_event": self.event1.pk,
},
)
with scopes_disabled():
ev = Event.objects.get(slug="33c3")
i_new = ev.items.first()
acr_new = ev.autocheckinrule_set.get()
assert i_new in acr_new.limit_products.all()
assert list(acr_new.limit_sales_channels.all()) == [
self.orga2.sales_channels.get(identifier="web")
]
assert acr_new.list.event == ev