mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Analyze check-in rules for missing products (#2582)
This commit is contained in:
@@ -93,6 +93,17 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="alert alert-info" v-if="missingItems.length">
|
||||
<p>
|
||||
{% trans "Your rule always filters by product or variation, but the following products or variations are not contained in any of your rule parts so people with these tickets will not get in:" %}
|
||||
</p>
|
||||
<ul>
|
||||
<li v-for="h in missingItems">{{ "{" }}{h}{{ "}" }}</li>
|
||||
</ul>
|
||||
<p>
|
||||
{% trans "Please double-check if this was intentional." %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="disabled-withoutjs sr-only">
|
||||
{{ form.rules }}
|
||||
@@ -105,6 +116,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{{ items|json_script:"items" }}
|
||||
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "vuejs/vue.js" %}"></script>
|
||||
@@ -118,6 +130,7 @@
|
||||
<script type="text/javascript" src="{% static "d3/d3-transition.v2.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "d3/d3-drag.v2.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "d3/d3-zoom.v2.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/checkinrules/jsonlogic-boolalg.js" %}"></script>
|
||||
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/datetimefield.vue' %}"></script>
|
||||
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/timefield.vue' %}"></script>
|
||||
<script type="text/vue" src="{% static 'pretixcontrol/js/ui/checkinrules/lookup-select2.vue' %}"></script>
|
||||
|
||||
@@ -313,6 +313,23 @@ class CheckinListUpdate(EventPermissionRequiredMixin, UpdateView):
|
||||
r['Content-Security-Policy'] = 'script-src \'unsafe-eval\''
|
||||
return r
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return {
|
||||
'items': [
|
||||
{
|
||||
'id': i.pk,
|
||||
'name': str(i),
|
||||
'variations': [
|
||||
{
|
||||
'id': v.pk,
|
||||
'name': str(v.value)
|
||||
} for v in i.variations.all()
|
||||
]
|
||||
} for i in self.request.event.items.filter(active=True).prefetch_related('variations')
|
||||
],
|
||||
**super().get_context_data(),
|
||||
}
|
||||
|
||||
def get_object(self, queryset=None) -> CheckinList:
|
||||
try:
|
||||
return self.request.event.checkin_lists.get(
|
||||
|
||||
@@ -35,7 +35,7 @@ def convert_to_dnf(rules):
|
||||
|
||||
def _distribute_or_over_and(r):
|
||||
operator = list(r.keys())[0]
|
||||
values = rules[operator]
|
||||
values = r[operator]
|
||||
if operator == "and":
|
||||
arg_to_distribute = [arg for arg in values if isinstance(arg, dict) and "or" in arg]
|
||||
if not arg_to_distribute:
|
||||
@@ -57,7 +57,7 @@ def convert_to_dnf(rules):
|
||||
if not isinstance(r, dict):
|
||||
return r
|
||||
operator = list(r.keys())[0]
|
||||
values = rules[operator]
|
||||
values = r[operator]
|
||||
if operator not in ("or", "and"):
|
||||
return r
|
||||
new_values = []
|
||||
|
||||
@@ -91,6 +91,9 @@ $(document).ready(function () {
|
||||
data: function () {
|
||||
return {
|
||||
rules: {},
|
||||
items: [],
|
||||
all_products: false,
|
||||
limit_products: [],
|
||||
TYPEOPS: TYPEOPS,
|
||||
VARS: VARS,
|
||||
texts: {
|
||||
@@ -108,8 +111,80 @@ $(document).ready(function () {
|
||||
hasRules: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
missingItems: function () {
|
||||
// This computed property contains list of item or variation names that
|
||||
// a) Are allowed on the checkin list according to all_products or include_products
|
||||
// b) Are not matched by ANY logical branch of the rule.
|
||||
// The list will be empty if there is a "catch-all" rule.
|
||||
var products_seen = {};
|
||||
var variations_seen = {};
|
||||
var rules = convert_to_dnf(this.rules);
|
||||
var branch_without_product_filter = false;
|
||||
|
||||
if (!rules["or"]) {
|
||||
rules = {"or": [rules]}
|
||||
}
|
||||
|
||||
for (var part of rules["or"]) {
|
||||
if (!part["and"]) {
|
||||
part = {"and": [part]}
|
||||
}
|
||||
var this_branch_without_product_filter = true;
|
||||
for (var subpart of part["and"]) {
|
||||
if (subpart["inList"]) {
|
||||
if (subpart["inList"][0]["var"] === "product" && subpart["inList"][1]) {
|
||||
this_branch_without_product_filter = false;
|
||||
for (var listentry of subpart["inList"][1]["objectList"]) {
|
||||
products_seen[parseInt(listentry["lookup"][1])] = true
|
||||
}
|
||||
} else if (subpart["inList"][0]["var"] === "variation" && subpart["inList"][1]) {
|
||||
this_branch_without_product_filter = false;
|
||||
for (var listentry_ of subpart["inList"][1]["objectList"]) {
|
||||
variations_seen[parseInt(listentry_["lookup"][1])] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this_branch_without_product_filter) {
|
||||
branch_without_product_filter = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (branch_without_product_filter || (!Object.keys(products_seen).length && !Object.keys(variations_seen).length)) {
|
||||
// At least one branch with no product filters at all – that's fine.
|
||||
return [];
|
||||
}
|
||||
|
||||
var missing = [];
|
||||
for (var item of this.items) {
|
||||
if (products_seen[item.id]) continue;
|
||||
if (!this.all_products && !this.limit_products.includes(item.id)) continue;
|
||||
if (item.variations.length > 0) {
|
||||
for (var variation of item.variations) {
|
||||
if (variations_seen[variation.id]) continue;
|
||||
missing.push(item.name + " – " + variation.name)
|
||||
}
|
||||
} else {
|
||||
missing.push(item.name)
|
||||
}
|
||||
}
|
||||
return missing;
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
this.rules = JSON.parse($("#id_rules").val());
|
||||
this.items = JSON.parse($("#items").html());
|
||||
|
||||
var root = this.$root
|
||||
function _update() {
|
||||
root.all_products = $("#id_all_products").prop("checked")
|
||||
root.limit_products = $("input[name=limit_products]:checked").map(function () { return parseInt($(this).val()) }).toArray()
|
||||
}
|
||||
$("#id_all_products, input[name=limit_products]").on("change", function () {
|
||||
_update();
|
||||
})
|
||||
_update()
|
||||
},
|
||||
watch: {
|
||||
rules: {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
function convert_to_dnf(rules) {
|
||||
// Converts a set of rules to disjunctive normal form, i.e. returns something of the form
|
||||
// `(a AND b AND c) OR (a AND d AND f)`
|
||||
// without further nesting.
|
||||
if (typeof rules !== "object" || Array.isArray(rules)) {
|
||||
return rules
|
||||
}
|
||||
|
||||
function _distribute_or_over_and(r) {
|
||||
var operator = Object.keys(r)[0]
|
||||
var values = r[operator]
|
||||
if (operator === "and") {
|
||||
var arg_to_distribute = null
|
||||
var other_args = []
|
||||
for (var arg of values) {
|
||||
if (typeof arg === "object" && !Array.isArray(arg) && typeof arg["or"] !== "undefined" && arg_to_distribute === null) {
|
||||
arg_to_distribute = arg
|
||||
} else {
|
||||
other_args.push(arg)
|
||||
}
|
||||
}
|
||||
if (arg_to_distribute === null) {
|
||||
return r
|
||||
}
|
||||
var or_operands = []
|
||||
for (var dval of arg_to_distribute["or"]) {
|
||||
or_operands.push({"and": other_args.concat([dval])})
|
||||
}
|
||||
return {
|
||||
"or": or_operands
|
||||
}
|
||||
} else if (!operator) {
|
||||
return r
|
||||
} else if (operator === "!" || operator === "!!" || operator === "?:" || operator === "if") {
|
||||
console.warn("Operator " + operator + " currently unsupported by convert_to_dnf")
|
||||
return r
|
||||
} else {
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
function _simplify_chained_operators(r) {
|
||||
// Simplify `(a OR b) OR (c or d)` to `a OR b OR c OR d` and the same with `AND`
|
||||
if (typeof r !== "object" || Array.isArray(r)) {
|
||||
return r
|
||||
}
|
||||
var operator = Object.keys(r)[0]
|
||||
var values = r[operator]
|
||||
if (operator !== "or" && operator !== "and") {
|
||||
return r
|
||||
}
|
||||
var new_values = []
|
||||
for (var v of values) {
|
||||
if (typeof v !== "object" || Array.isArray(v) || typeof v[operator] === "undefined") {
|
||||
new_values.push(v)
|
||||
} else {
|
||||
new_values.push(...v[operator])
|
||||
}
|
||||
}
|
||||
var result = {}
|
||||
result[operator] = new_values
|
||||
return result
|
||||
}
|
||||
|
||||
// Run _distribute_or_over_and on until it no longer changes anything. Do so recursively
|
||||
// for the full expression tree.
|
||||
var old_rules = rules
|
||||
while (true) {
|
||||
rules = _distribute_or_over_and(rules)
|
||||
var operator = Object.keys(rules)[0]
|
||||
var values = rules[operator]
|
||||
var no_list = false
|
||||
if (!Array.isArray(values)) {
|
||||
values = [values]
|
||||
no_list = true
|
||||
}
|
||||
rules = {}
|
||||
if (!no_list) {
|
||||
rules[operator] = []
|
||||
for (var v of values) {
|
||||
rules[operator].push(convert_to_dnf(v))
|
||||
}
|
||||
} else {
|
||||
rules[operator] = convert_to_dnf(values[0])
|
||||
}
|
||||
if (JSON.stringify(old_rules) === JSON.stringify(rules)) { // Let's hope this is good enough...
|
||||
break
|
||||
}
|
||||
old_rules = rules
|
||||
}
|
||||
rules = _simplify_chained_operators(rules)
|
||||
return rules
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
</rect>
|
||||
<foreignObject :width="boxWidth - 10" :height="boxHeight - 10" :x="x + 5" :y="y + 5">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" class="text">
|
||||
<span v-if="vardata.type === 'int'">
|
||||
<span v-if="vardata && vardata.type === 'int'">
|
||||
<span v-if="variable.startsWith('entries_')" class="fa fa-sign-in"></span>
|
||||
{{ vardata.label }}
|
||||
<br>
|
||||
@@ -15,7 +15,7 @@
|
||||
{{ op.label }} {{ rightoperand }}
|
||||
</strong>
|
||||
</span>
|
||||
<span v-else-if="variable === 'now'">
|
||||
<span v-else-if="vardata && variable === 'now'">
|
||||
<span class="fa fa-clock-o"></span> {{ vardata.label }}<br>
|
||||
<strong>
|
||||
{{ op.label }}<br>
|
||||
@@ -36,7 +36,7 @@
|
||||
</span>
|
||||
</strong>
|
||||
</span>
|
||||
<span v-else-if="operator === 'inList'">
|
||||
<span v-else-if="vardata && operator === 'inList'">
|
||||
<span class="fa fa-ticket"></span> {{ vardata.label }}<br>
|
||||
<strong>
|
||||
{{ rightoperand.objectList.map((o) => o.lookup[2]).join(", ") }}
|
||||
@@ -53,6 +53,7 @@
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
|
||||
props: {
|
||||
node: Object,
|
||||
nodeid: String,
|
||||
|
||||
Reference in New Issue
Block a user