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

@@ -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)