From 22920a731825a0b80a6a5a75abb8928789998bc2 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Mon, 11 Apr 2022 18:53:07 +0200 Subject: [PATCH] Analyze check-in rules for missing products (#2582) --- .../pretixcontrol/checkin/list_edit.html | 13 +++ src/pretix/control/views/checkin.py | 17 ++++ src/pretix/helpers/jsonlogic_boolalg.py | 4 +- .../pretixcontrol/js/ui/checkinrules.js | 75 +++++++++++++++ .../js/ui/checkinrules/jsonlogic-boolalg.js | 93 +++++++++++++++++++ .../js/ui/checkinrules/viz-node.vue | 7 +- 6 files changed, 204 insertions(+), 5 deletions(-) create mode 100644 src/pretix/static/pretixcontrol/js/ui/checkinrules/jsonlogic-boolalg.js diff --git a/src/pretix/control/templates/pretixcontrol/checkin/list_edit.html b/src/pretix/control/templates/pretixcontrol/checkin/list_edit.html index 54ab0e45d..2a61fe643 100644 --- a/src/pretix/control/templates/pretixcontrol/checkin/list_edit.html +++ b/src/pretix/control/templates/pretixcontrol/checkin/list_edit.html @@ -93,6 +93,17 @@ +
+

+ {% 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:" %} +

+ +

+ {% trans "Please double-check if this was intentional." %} +

+
{{ form.rules }} @@ -105,6 +116,7 @@
+ {{ items|json_script:"items" }} {% compress js %} @@ -118,6 +130,7 @@ + diff --git a/src/pretix/control/views/checkin.py b/src/pretix/control/views/checkin.py index e3218c176..33d163bf3 100644 --- a/src/pretix/control/views/checkin.py +++ b/src/pretix/control/views/checkin.py @@ -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( diff --git a/src/pretix/helpers/jsonlogic_boolalg.py b/src/pretix/helpers/jsonlogic_boolalg.py index bd9a87157..b879fe591 100644 --- a/src/pretix/helpers/jsonlogic_boolalg.py +++ b/src/pretix/helpers/jsonlogic_boolalg.py @@ -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 = [] diff --git a/src/pretix/static/pretixcontrol/js/ui/checkinrules.js b/src/pretix/static/pretixcontrol/js/ui/checkinrules.js index 68d18bbad..45a811473 100644 --- a/src/pretix/static/pretixcontrol/js/ui/checkinrules.js +++ b/src/pretix/static/pretixcontrol/js/ui/checkinrules.js @@ -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: { diff --git a/src/pretix/static/pretixcontrol/js/ui/checkinrules/jsonlogic-boolalg.js b/src/pretix/static/pretixcontrol/js/ui/checkinrules/jsonlogic-boolalg.js new file mode 100644 index 000000000..8354ccf82 --- /dev/null +++ b/src/pretix/static/pretixcontrol/js/ui/checkinrules/jsonlogic-boolalg.js @@ -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 +} \ No newline at end of file diff --git a/src/pretix/static/pretixcontrol/js/ui/checkinrules/viz-node.vue b/src/pretix/static/pretixcontrol/js/ui/checkinrules/viz-node.vue index 2bc60b591..81c398daa 100644 --- a/src/pretix/static/pretixcontrol/js/ui/checkinrules/viz-node.vue +++ b/src/pretix/static/pretixcontrol/js/ui/checkinrules/viz-node.vue @@ -7,7 +7,7 @@
- + {{ vardata.label }}
@@ -15,7 +15,7 @@ {{ op.label }} {{ rightoperand }}
- + {{ vardata.label }}
{{ op.label }}
@@ -36,7 +36,7 @@
- + {{ vardata.label }}
{{ rightoperand.objectList.map((o) => o.lookup[2]).join(", ") }} @@ -53,6 +53,7 @@