");
- var qrText = this.getAttribute("type") === "application/json" && this.textContent.startsWith('"') ?
- JSON.parse(this.textContent) : $(this).html();
- $div.insertBefore($(this));
- $div.qrcode(
- {
- text: qrText,
- correctLevel: 0, // M
- width: $(this).attr("data-size") ? parseInt($(this).attr("data-size")) : 256,
- height: $(this).attr("data-size") ? parseInt($(this).attr("data-size")) : 256,
- }
- ).find("canvas").attr("role", "img").attr("aria-label", this.getAttribute("data-desc"));
- });
-
-
- el.find("fieldset[data-addon-max-count]").each(function() {
- // usually addons are only allowed once one per item
- var multipleAllowed = this.hasAttribute("data-addon-multi-allowed");
- var $inputs = $(".availability-box input", this);
- var max = parseInt(this.getAttribute("data-addon-max-count"));
- var desc = $(".addon-count-desc", this).text().trim();
- this.addEventListener("change", function (e) {
- var variations = e.target.closest(".variations");
- if (variations && !multipleAllowed && e.target.checked) {
- // uncheck all other checkboxes inside this variations
- $(".availability-box input:checked", variations).not(e.target).prop("checked", false).trigger("change");
- }
-
- if (max === 1) {
- if (e.target.checked) {
- $inputs.filter(":checked").not(e.target).prop("checked", false).trigger("change");
- }
- return;
- }
- var total = $inputs.toArray().reduce(function(a, e) {
- return a + (e.type == "checkbox" ? (e.checked ? parseInt(e.value) : 0) : parseInt(e.value) || 0);
- }, 0);
- if (total > max) {
- if (e.target.type == "checkbox") {
- e.target.checked = false;
- } else {
- e.target.value = e.target.value - (total - max);
- }
- $(e.target).trigger("change").closest(".availability-box").tooltip({
- "title": desc,
- }).tooltip('show');
- e.preventDefault();
+ function wait() {
+ time.attr("datetime", s+"s");
+ if (s > 0) {
+ time.text("(" + reltf.format(s, "seconds") + ")");
+ window.setTimeout(wait, 1000);
+ s--;
} else {
- $(".availability-box", this).tooltip('destroy')
+ time.remove();
+ input.disabled = false;
+ input.classList.remove("disabled");
+ input.removeEventListener("click", disable_submit);
}
- });
- });
-
- el.find("input[name*=question], select[name*=question]").change(questions_toggle_dependent);
- questions_toggle_dependent();
- questions_init_photos(el);
-
- var cancel_fee_slider_update = function () {
- if (typeof django === "undefined") {
- window.setTimeout(cancel_fee_slider_update, 100);
- return;
- }
- $("#cancel-fee-keep").text(interpolate(
- gettext("The organizer keeps %(currency)s %(amount)s"),
- {
- 'currency': $("body").attr("data-currency"),
- 'amount': floatformat(cancel_fee_slider.getValue(), 2)
- },
- true
- ));
- $("#cancel-fee-refund").text(interpolate(
- gettext("You get %(currency)s %(amount)s back"),
- {
- 'currency': $("body").attr("data-currency"),
- 'amount': floatformat((cancel_fee_slider.getAttribute("max") - cancel_fee_slider.getValue()), 2)
- },
- true
- ));
- }
- var cancel_fee_slider = el.find('#cancel-fee-slider').slider({
- }).on('slide', function () {
- cancel_fee_slider_update();
- }).data('slider');
- if (cancel_fee_slider) {
- cancel_fee_slider_update();
- el.find("#cancel-fee-custom").click(function () {
- try {
- var newinp = parseFloat(prompt(gettext("Please enter the amount the organizer can keep."), cancel_fee_slider.getValue().toString()).replace(',', '.'));
- cancel_fee_slider.setValue(newinp);
- cancel_fee_slider_update();
- } catch (e) {
- }
- });
- }
-
- el.find('.use_giftcard').on("click", function () {
- var value = $(this).data('value');
- $('#id_payment_giftcard-code').val(value)
- })
-
-};
-
-function setup_basics(el) {
- el.find("form").attr("novalidate", true).on("submit", function (e) {
- if (!this.checkValidity()) {
- var input = this.querySelector(":invalid:not(fieldset)");
- (input.labels[0] || input).scrollIntoView();
- // only use reportValidity, which usually sets focus on element
- // input.focus() opens dropdowns, which is not what we want
- input.reportValidity();
- e.preventDefault();
}
+ wait();
});
- el.find("input[data-toggle=radiocollapse]").change(function () {
- $($(this).attr("data-parent")).find(".collapse.in").collapse('hide');
- $($(this).attr("data-target")).collapse('show');
- });
- el.find("input[data-toggle=radiocollapse]:checked").each(function () {
- if (!$($(this).attr("data-parent")).find(".collapse.in").length) {
- $($(this).attr("data-target")).collapse('show');
- }
- });
- $("fieldset.accordion-panel > legend input[type=radio]").change(function() {
- $(this).closest("fieldset").siblings("fieldset").prop('disabled', true).children('.panel-body').slideUp();
- $(this).closest("fieldset").prop('disabled', false).children('.panel-body').slideDown();
- }).filter(':not(:checked)').each(function() { $(this).closest("fieldset").prop('disabled', true).children('.panel-body').hide(); });
+ el.find('.input-item-count-dec, .input-item-count-inc').on('click', function (e) {
+ e.preventDefault()
+ let step = parseFloat(this.getAttribute('data-step'))
+ let controls = document.getElementById(this.getAttribute('data-controls'))
+ let currentValue = parseFloat(controls.value)
+ controls.value = Math.max(controls.min, Math.min(controls.max || Number.MAX_SAFE_INTEGER, (currentValue || 0) + step))
+ controls.dispatchEvent(new Event('change', { bubbles: true }))
+ })
+ el.find('.btn-checkbox input').on('change', function (e) {
+ $(this).closest('.btn-checkbox')
+ .toggleClass('btn-checkbox-checked', this.checked)
+ .find('.fa').toggleClass('fa-shopping-cart', !this.checked).toggleClass('fa-cart-arrow-down', this.checked)
+ })
+ el.find('.btn-checkbox:has([checked])')
+ .addClass('btn-checkbox-checked')
+ .find('.fa-shopping-cart')
+ .removeClass('fa-shopping-cart')
+ .addClass('fa-cart-arrow-down')
- el.find(".js-only").removeClass("js-only");
- el.find(".js-hidden").hide();
+ el.find('script[data-replace-with-qr]').each(function () {
+ let $div = $('
'
- ),
- data: shared_widget_data,
- methods: shared_methods,
-});
-
-/* Function to create the actual Vue instances */
-
-var shared_root_methods = {
- open_link_in_frame: function (event) {
- var url = event.target.attributes.href.value;
- if (this.$root.additionalURLParams) {
- if (url.indexOf('?')) {
- url += '&' + this.$root.additionalURLParams;
- } else {
- url += '?' + this.$root.additionalURLParams;
- }
- }
- if (this.$root.useIframe) {
- event.preventDefault();
- if (url.indexOf('?')) {
- url += '&iframe=1';
- } else {
- url += '?iframe=1';
- }
- this.$root.overlay.frame_src = url;
- } else {
- event.target.href = url;
- return;
- }
- },
- trigger_load_callback: function () {
- this.$nextTick(function () {
- for (var i = 0; i < window.PretixWidget._loaded.length; i++) {
- window.PretixWidget._loaded[i]()
- }
- });
- },
- trigger_close_callback: function () {
- this.$nextTick(function () {
- for (var i = 0; i < window.PretixWidget._closed.length; i++) {
- window.PretixWidget._closed[i]()
- }
- });
- },
- reload: function (opt = {}) {
- var url;
- if (this.$root.is_button) {
- return;
- }
- if (this.$root.subevent) {
- url = this.$root.target_url + this.$root.subevent + '/widget/product_list?lang=' + lang;
- } else {
- url = this.$root.target_url + 'widget/product_list?lang=' + lang;
- }
- if (this.$root.offset) {
- url += '&offset=' + this.$root.offset;
- }
- if (this.$root.filter) {
- url += '&' + this.$root.filter;
- }
- if (this.$root.item_filter) {
- url += '&items=' + encodeURIComponent(this.$root.item_filter);
- }
- if (this.$root.category_filter) {
- url += '&categories=' + encodeURIComponent(this.$root.category_filter);
- }
- if (this.$root.variation_filter) {
- url += '&variations=' + encodeURIComponent(this.$root.variation_filter);
- }
- if (this.$root.voucher_code) {
- url += '&voucher=' + encodeURIComponent(this.$root.voucher_code);
- }
- if (this.$root.get_cart_id()) {
- url += "&cart_id=" + encodeURIComponent(this.$root.get_cart_id());
- }
- if (this.$root.date !== null) {
- url += "&date=" + this.$root.date.substr(0, 7);
- } else if (this.$root.week !== null) {
- url += "&date=" + this.$root.week[0] + "-W" + this.$root.week[1];
- }
- if (this.$root.style !== null) {
- url = url + '&style=' + encodeURIComponent(this.$root.style);
- }
- var root = this.$root;
- api._getJSON(url, function (data, xhr) {
- if (typeof xhr.responseURL !== "undefined") {
- var new_url = xhr.responseURL.substr(0, xhr.responseURL.indexOf("/widget/product_list?") + 1);
- var old_url = url.substr(0, url.indexOf("/widget/product_list?") + 1);
- if (new_url !== old_url) {
- if (root.subevent) {
- new_url = new_url.substr(0, new_url.lastIndexOf("/", new_url.length - 1) + 1);
- }
- root.target_url = new_url;
- root.reload();
- return;
- }
- }
- root.connection_error = false;
- if (data.weeks !== undefined) {
- root.weeks = data.weeks;
- root.date = data.date;
- root.week = null;
- root.events = undefined;
- root.view = "weeks";
- root.name = data.name;
- root.frontpage_text = data.frontpage_text;
- root.meta_filter_fields = data.meta_filter_fields;
- } else if (data.days !== undefined) {
- root.days = data.days;
- root.date = null;
- root.week = data.week;
- root.events = undefined;
- root.view = "days";
- root.name = data.name;
- root.frontpage_text = data.frontpage_text;
- root.meta_filter_fields = data.meta_filter_fields;
- } else if (data.events !== undefined) {
- root.events = root.append_events && root.events ? root.events.concat(data.events) : data.events;
- root.append_events = false;
- root.weeks = undefined;
- root.view = "events";
- root.name = data.name;
- root.frontpage_text = data.frontpage_text;
- root.has_more_events = data.has_more_events;
- root.meta_filter_fields = data.meta_filter_fields;
- } else {
- root.view = "event";
- // Replace target_url and subevent with canonical values in case they were slightly wrong
- root.target_url = data.target_url;
- root.subevent = data.subevent;
- // Event data
- root.name = data.name;
- root.frontpage_text = data.frontpage_text;
- root.date_range = data.date_range;
- root.location = data.location;
- root.categories = data.items_by_category;
- root.currency = data.currency;
- root.display_net_prices = data.display_net_prices;
- root.voucher_explanation_text = data.voucher_explanation_text;
- root.error = data.error;
- root.display_add_to_cart = data.display_add_to_cart;
- root.waiting_list_enabled = data.waiting_list_enabled;
- root.show_variations_expanded = data.show_variations_expanded || !!root.variation_filter;
- root.cart_exists = data.cart_exists;
- root.vouchers_exist = data.vouchers_exist;
- root.has_seating_plan = data.has_seating_plan;
- root.has_seating_plan_waitinglist = data.has_seating_plan_waitinglist;
- root.itemnum = data.itemnum;
- }
- root.poweredby = data.poweredby;
- if (root.loading > 0) {
- root.loading--;
- root.trigger_load_callback();
- }
- if (root.parent_stack.length > 0 && root.has_seating_plan && root.categories.length === 0 && !root.frame_dismissed && root.useIframe && !root.error && !root.has_seating_plan_waitinglist) {
- // If we're on desktop and someone selects a seating-only event in a calendar, let's open it right away,
- // but only if the person didn't close it before.
- root.startseating()
- } else {
- // make sure to only move focus to content element when it had focus before the reload/click
- // this is needed because reload is also called on initial load and we do not want to move focus on initial load
- if (root.$el.contains(document.activeElement)) {
- root.$nextTick(function() {
- // wait for redraw, then focus content element for better a11y
- (opt.focus ? document.querySelector(opt.focus) : root.$el).focus();
- });
- }
- }
- }, function (error) {
- root.categories = [];
- root.currency = '';
- if (error.status === 429) {
- root.error = strings['loading_error_429'];
- root.connection_error = true;
- } else {
- root.error = strings['loading_error'];
- root.connection_error = true;
- }
- if (root.loading > 0) {
- root.loading--;
- root.trigger_load_callback();
- }
- });
- },
- startwaiting: function () {
- var redirect_url = this.$root.target_url + 'w/' + widget_id + '/waitinglist/?iframe=1&locale=' + lang;
- if (this.$root.subevent){
- redirect_url += '&subevent=' + this.$root.subevent;
- }
- if (this.$root.additionalURLParams) {
- redirect_url += '&' + this.$root.additionalURLParams;
- }
- if (this.$root.useIframe) {
- this.$root.overlay.frame_src = redirect_url;
- } else {
- window.open(redirect_url);
- }
- },
- startseating: function () {
- var redirect_url = this.$root.target_url + 'w/' + widget_id;
- if (this.$root.subevent){
- redirect_url += '/' + this.$root.subevent;
- }
- redirect_url += '/seatingframe/?iframe=1&locale=' + lang;
- if (this.$root.voucher_code) {
- redirect_url += '&voucher=' + encodeURIComponent(this.$root.voucher_code);
- }
- if (this.$root.get_cart_id()) {
- redirect_url += '&take_cart_id=' + encodeURIComponent(this.$root.get_cart_id());
- }
- if (this.$root.widget_data) {
- redirect_url += '&widget_data=' + encodeURIComponent(this.$root.widget_data_json);
- }
- if (this.$root.additionalURLParams) {
- redirect_url += '&' + this.$root.additionalURLParams;
- }
- redirect_url += this.$root.consent_parameter;
- if (this.$root.useIframe) {
- this.$root.overlay.frame_src = redirect_url;
- } else {
- window.open(redirect_url);
- }
- },
- choose_event: function (event) {
- this.$root.target_url = event.event_url;
- this.$root.error = null;
- this.$root.connection_error = false;
- this.$root.subevent = event.subevent;
- this.$root.loading++;
- this.$root.reload();
- },
- create_cart: function(callback) {
- var url = this.$root.target_url + 'w/' + widget_id + '/cart/create?ajax=1';
-
- this.$root.overlay.frame_loading = true;
- api._getJSON(url, (data) => {
- this.$root.set_cart_id(data.cart_id);
- this.$root.overlay.frame_loading = false;
- callback()
- }, (xhr, data) => {
- if (xhr.status === 429 && typeof xhr.responseURL !== "undefined") {
- this.$root.overlay.error_message = strings['cart_error_429'];
- this.$root.overlay.frame_loading = false;
- this.$root.overlay.error_url_after = this.$root.newTabTarget;
- this.$root.overlay.error_url_after_new_tab = true;
- } else {
- this.$root.overlay.error_message = strings['cart_error'];
- this.$root.overlay.frame_loading = false;
- }
- })
- },
- get_cart_id: function() {
- if (!this.$root.keep_cart) {
- return null
- }
- if (this.$root.cart_id) {
- return this.$root.cart_id
- }
- return getCookie(this.$root.cookieName);
- },
- set_cart_id: function(newValue) {
- this.$root.cart_id = newValue
- setCookie(this.$root.cookieName, newValue, 30);
- },
-};
-
-var shared_root_computed = {
- cookieName: function () {
- return "pretix_widget_" + this.target_url.replace(/[^a-zA-Z0-9]+/g, "_");
- },
- formTarget: function () {
- var is_firefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
- var is_android = navigator.userAgent.toLowerCase().indexOf("android") > -1;
- if (is_android && is_firefox) {
- // Opening a POST form in a new browser fails in Firefox. This is supposed to be fixed since FF 76
- // but for some reason, it is still the case in FF for Android.
- // https://bugzilla.mozilla.org/show_bug.cgi?id=1629441
- // https://github.com/pretix/pretix/issues/1040
- return "_top";
- } else {
- return "_blank";
- }
- },
- voucherFormTarget: function () {
- var form_target = this.target_url + 'w/' + widget_id + '/redeem?iframe=1&locale=' + lang;
- if (this.get_cart_id()) {
- form_target += "&take_cart_id=" + encodeURIComponent(this.get_cart_id());
- }
- if (this.subevent) {
- form_target += "&subevent=" + this.subevent;
- }
- if (this.$root.widget_data) {
- form_target += '&widget_data=' + encodeURIComponent(this.$root.widget_data_json);
- }
- form_target += this.$root.consent_parameter;
- if (this.$root.additionalURLParams) {
- form_target += '&' + this.$root.additionalURLParams;
- }
- return form_target;
- },
- formMethod: function () {
- if (!this.useIframe && this.is_button && this.items.length === 0) {
- return 'get';
- }
- return 'post';
- },
- formAction: function () {
- if (!this.useIframe && this.is_button && this.items.length === 0) {
- var target;
- if (this.voucher_code) {
- target = this.target_url + 'redeem';
- } else if (this.subevent) {
- target = this.target_url + this.subevent + '/';
- } else {
- target = this.target_url;
- }
- return target;
- }
- var checkout_url = "/" + this.target_url.replace(/^[^\/]+:\/\/([^\/]+)\//, "") + "w/" + widget_id + "/";
- if (!this.$root.cart_exists) {
- checkout_url += "checkout/start";
- }
- if (this.$root.additionalURLParams) {
- checkout_url += '?' + this.$root.additionalURLParams;
- }
- var form_target = this.target_url + 'w/' + widget_id + '/cart/add?iframe=1&next=' + encodeURIComponent(checkout_url);
- if (this.get_cart_id()) {
- form_target += "&take_cart_id=" + encodeURIComponent(this.get_cart_id());
- }
- form_target += this.$root.consent_parameter
- return form_target
- },
- newTabTarget: function () {
- var target = this.target_url;
- if (this.subevent) {
- target = this.target_url + this.subevent + '/';
- }
- var parameters = this.$root.consent_parameter
- if (this.$root.additionalURLParams) {
- parameters += `&${this.$root.additionalURLParams}`
- }
- if (parameters) {
- target += '?' + parameters.replace(/^&/, '')
- }
- return target
- },
- useIframe: function () {
- if (window.crossOriginIsolated === true) {
- console.warn("pretix Widget cannot use iframe due to Cross-Origin-Embed-Policy")
- return false;
- }
- return !this.disable_iframe && (this.skip_ssl || site_is_secure());
- },
- showPrices: function () {
- var has_priced = false;
- var cnt_items = 0;
- for (var i = 0; i < this.categories.length; i++) {
- for (var j = 0; j < this.categories[i].items.length; j++) {
- var item = this.categories[i].items[j];
- if (item.has_variations) {
- cnt_items += item.variations.length;
- has_priced = true;
- } else {
- cnt_items++;
- has_priced = has_priced || item.price.gross != "0.00" || item.free_price;
- }
- }
- }
- return has_priced || cnt_items > 1;
- },
- consent_parameter_value: function () {
- if (typeof this.widget_data["consent"] !== "undefined") {
- return encodeURIComponent(this.widget_data["consent"]);
- }
- return "";
- },
- consent_parameter: function () {
- if (typeof this.widget_data["consent"] !== "undefined") {
- return "&consent=" + encodeURIComponent(this.widget_data["consent"]);
- }
- return "";
- },
- widget_data_json: function () {
- var cloned_data = Object.assign({}, this.widget_data);
- if (typeof cloned_data["consent"] !== "undefined") {
- // Remove consent as we pass it differently. We still keep it as widget_data in the input to avoid breaking
- // the JS API of the widget.
- delete cloned_data["consent"];
- }
- return JSON.stringify(cloned_data);
- },
- additionalURLParams: function () {
- if (!window.location.search.indexOf('utm_')) {
- return '';
- }
- var params = new URLSearchParams(window.location.search);
- for (var [key, value] of params.entries()) {
- if (!key.startsWith('utm_')) {
- params.delete(key);
- }
- }
- return params.toString();
- },
-};
-
-var create_overlay = function (app) {
- var elem = document.createElement('pretix-overlay');
- document.body.appendChild(elem);
-
- var framechild = new Vue({
- el: elem,
- data: function () {
- return {
- parent: app,
- frame_loading: false,
- frame_shown: false,
- error_url_after: null,
- error_url_after_new_tab: true,
- error_message: null,
- lightbox: null,
- prevActiveElement: null,
- }
- },
- props: {
- frame_src: String,
- },
- methods: {
- },
- watch: {
- frame_src: function (newValue, oldValue) {
- // show loading spinner only when previously no frame_src was set
- if (newValue && !oldValue) {
- this.frame_loading = true;
- }
- // to close and unload the iframe, frame_src can be empty -> make it valid HTML with about:blank
- this.$el.querySelector("iframe").src = newValue || "about:blank";
- },
- frame_loading: function (newValue) {
- var dialog = this.$el?.querySelector('dialog.pretix-widget-frame-holder');
- if (newValue) {
- if (!dialog.open) {
- dialog.showModal();
- }
- } else {
- if (!this.frame_src && dialog.open) {// finished loading, but no iframe to display => close
- dialog.close();
- }
- }
- },
- }
- });
- app.$root.overlay = framechild;
-};
-
-function get_ga_client_id(tracking_id) {
- if (typeof ga === "undefined") {
- return null;
- }
- try {
- var trackers = ga.getAll();
- var i, len;
- for (i = 0, len = trackers.length; i < len; i += 1) {
- if (trackers[i].get('trackingId') === tracking_id) {
- return trackers[i].get('clientId');
- }
- }
- } catch (e) {
- }
- return null;
-}
-
-var create_widget = function (element, html_id=null) {
- var target_url = element.attributes.event.value;
- if (!target_url.match(/\/$/)) {
- target_url += "/";
- }
- var voucher = element.attributes.voucher ? element.attributes.voucher.value : null;
- var subevent = element.attributes.subevent ? element.attributes.subevent.value : null;
- var style = element.attributes["list-type"] ? element.attributes["list-type"].value : (element.attributes.style ? element.attributes.style.value : null);
- var skip_ssl = element.attributes["skip-ssl-check"] ? true : false;
- var disable_iframe = element.attributes["disable-iframe"] ? true : false;
- var disable_vouchers = element.attributes["disable-vouchers"] ? true : false;
- var disable_filters = element.attributes["disable-filters"] ? true : false;
- var display_event_info = element.getAttribute("display-event-info"); // null means "auto" (as before), everything other than "false" is true
- if (display_event_info !== null && display_event_info !== "auto") {
- display_event_info = display_event_info !== "false";
- } else {
- display_event_info = null;
- }
- var widget_data = JSON.parse(JSON.stringify(window.PretixWidget.widget_data));
- var filter = element.attributes.filter ? element.attributes.filter.value : null;
- var items = element.attributes.items ? element.attributes.items.value : null;
- var variations = element.attributes.variations ? element.attributes.variations.value : null;
- var categories = element.attributes.categories ? element.attributes.categories.value : null;
- for (var i = 0; i < element.attributes.length; i++) {
- var attrib = element.attributes[i];
- if (attrib.name.match(/^data-.*$/)) {
- widget_data[attrib.name.replace(/^data-/, '')] = attrib.value;
- }
- }
- html_id = html_id || element.id || makeid(16);
-
- var observer = new MutationObserver((mutationList) => {
- mutationList.forEach((mutation) => {
- if (mutation.type == "attributes" && mutation.attributeName.startsWith("data-")) {
- Vue.set(app.widget_data, mutation.attributeName.substring(5), mutation.target.getAttribute(mutation.attributeName));
- }
- });
- });
- var observerOptions = { attributes: true };
-
- if (element.tagName !== "pretix-widget") {
- element.innerHTML = "
";
- // we need to watch the container as well as the replaced root-node (see mounted())
- observer.observe(element, observerOptions);
- }
-
- var app = new Vue({
- el: element,
- data: function () {
- return {
- target_url: target_url,
- parent_stack: [],
- subevent: subevent,
- is_button: false,
- categories: null,
- currency: null,
- name: null,
- date_range: null,
- location: null,
- offset: 0,
- has_more_events: false,
- append_events: false,
- frontpage_text: null,
- filter: filter,
- item_filter: items,
- category_filter: categories,
- variation_filter: variations,
- voucher_code: voucher,
- display_net_prices: false,
- voucher_explanation_text: null,
- show_variations_expanded: !!variations,
- skip_ssl: skip_ssl,
- disable_iframe: disable_iframe,
- style: style,
- connection_error: false,
- error: null,
- weeks: null,
- days: null,
- date: null,
- week: null,
- frame_dismissed: false,
- events: null,
- view: null,
- display_add_to_cart: false,
- widget_data: widget_data,
- loading: 1,
- widget_id: 'pretix-widget-' + widget_id,
- html_id: html_id,
- vouchers_exist: false,
- disable_vouchers: disable_vouchers,
- disable_filters: disable_filters,
- display_event_info: display_event_info,
- cart_exists: false,
- itemcount: 0,
- overlay: null,
- poweredby: "",
- has_seating_plan: false,
- has_seating_plan_waitinglist: false,
- meta_filter_fields: [],
- keep_cart: true,
- cart_id: null
- }
- },
- created: function () {
- this.reload();
- },
- mounted: function () {
- observer.observe(this.$el, observerOptions);
- },
- computed: shared_root_computed,
- methods: shared_root_methods,
- watch: {
- 'view': function (newValue, oldValue) {
- if (oldValue) {
- // always make sure the widget is scrolled to the top
- // as we only check top, we do not need to wait for a redraw
- var rect = this.$el.getBoundingClientRect();
- if (rect.top < 0) {
- this.$el.scrollIntoView();
- }
- }
- }
- }
- });
- create_overlay(app);
- return app;
-};
-
-var create_button = function (element, html_id=null) {
- var target_url = element.attributes.event.value;
- if (!target_url.match(/\/$/)) {
- target_url += "/";
- }
- var voucher = element.attributes.voucher ? element.attributes.voucher.value : null;
- var subevent = element.attributes.subevent ? element.attributes.subevent.value : null;
- var raw_items = element.attributes.items ? element.attributes.items.value : "";
- var skip_ssl = element.attributes["skip-ssl-check"] ? true : false;
- var disable_iframe = element.attributes["disable-iframe"] ? true : false;
- var keep_cart = element.attributes["keep-cart"] ? true : false;
- var button_text = element.innerHTML;
- var widget_data = JSON.parse(JSON.stringify(window.PretixWidget.widget_data));
- for (var i = 0; i < element.attributes.length; i++) {
- var attrib = element.attributes[i];
- if (attrib.name.match(/^data-.*$/)) {
- widget_data[attrib.name.replace(/^data-/, '')] = attrib.value;
- }
- }
- html_id = html_id || element.id || makeid(16);
-
- var observer = new MutationObserver((mutationList) => {
- mutationList.forEach((mutation) => {
- if (mutation.type == "attributes" && mutation.attributeName.startsWith("data-")) {
- Vue.set(app.widget_data, mutation.attributeName.substring(5), mutation.target.getAttribute(mutation.attributeName));
- }
- });
- });
- var observerOptions = { attributes: true };
-
- if (element.tagName !== "pretix-button") {
- element.innerHTML = "
" + element.innerHTML + "";
- // Vue does not replace the container, so watch container as well
- observer.observe(element, observerOptions);
- }
-
- var itemsplit = raw_items.split(",");
- var items = [];
- for (var i = 0; i < itemsplit.length; i++) {
- if (itemsplit[i].indexOf("=") > 0 ) {
- var splitthis = itemsplit[i].split("=");
- items.push({'item': splitthis[0], 'count': splitthis[1]})
- }
- }
-
- var app = new Vue({
- el: element,
- data: function () {
- return {
- target_url: target_url,
- subevent: subevent,
- is_button: true,
- skip_ssl: skip_ssl,
- disable_iframe: disable_iframe,
- voucher_code: voucher,
- items: items,
- error: null,
- filter: null,
- frame_dismissed: false,
- widget_data: widget_data,
- widget_id: 'pretix-widget-' + widget_id,
- html_id: html_id,
- button_text: button_text,
- keep_cart: keep_cart || items.length > 0,
- cart_id: null
- }
- },
- created: function () {
- },
- mounted: function () {
- observer.observe(this.$el, observerOptions);
- },
- computed: shared_root_computed,
- methods: shared_root_methods,
- });
- create_overlay(app);
- return app;
-};
-
-/* Find all widgets on the page and render them */
-widgetlist = [];
-buttonlist = [];
-window.PretixWidget._loaded = [];
-window.PretixWidget._closed = [];
-window.PretixWidget.addLoadListener = function (f) {
- window.PretixWidget._loaded.push(f);
-}
-window.PretixWidget.addCloseListener = function (f) {
- window.PretixWidget._closed.push(f);
-}
-window.PretixWidget.buildWidgets = function () {
- document.createElement("pretix-widget");
- document.createElement("pretix-button");
- docReady(function () {
- var widgets = document.querySelectorAll("pretix-widget, div.pretix-widget-compat");
- var wlength = widgets.length;
- for (var i = 0; i < wlength; i++) {
- var widget = widgets[i];
- widgetlist.push(create_widget(widget, widget.id || "pretix-widget-"+i));
- }
-
- var buttons = document.querySelectorAll("pretix-button, div.pretix-button-compat");
- var blength = buttons.length;
- for (var i = 0; i < blength; i++) {
- var button = buttons[i];
- buttonlist.push(create_button(button, button.id || "pretix-button-"+i));
- }
- });
-};
-
-window.PretixWidget.open = function (target_url, voucher, subevent, items, widget_data, skip_ssl_check, disable_iframe) {
- if (!target_url.match(/\/$/)) {
- target_url += "/";
- }
-
- var all_widget_data = JSON.parse(JSON.stringify(window.PretixWidget.widget_data));
- if (widget_data) {
- Object.keys(widget_data).forEach(function(key) { all_widget_data[key] = widget_data[key]; });
- }
- var root = document.createElement("div");
- document.body.appendChild(root);
- root.classList.add("pretix-widget-hidden");
- root.innerHTML = "
";
- var app = new Vue({
- el: root,
- data: function () {
- return {
- target_url: target_url,
- subevent: subevent || null,
- is_button: true,
- skip_ssl: skip_ssl_check || false,
- disable_iframe: disable_iframe || false,
- voucher_code: voucher || null,
- items: items || [],
- error: null,
- filter: null,
- frame_dismissed: false,
- widget_data: all_widget_data,
- widget_id: 'pretix-widget-' + widget_id,
- button_text: "",
- keep_cart: true,
- cart_id: null
- }
- },
- created: function () {
- },
- computed: shared_root_computed,
- methods: shared_root_methods,
- });
- create_overlay(app);
- app.$nextTick(function () {
- if (this.$root.useIframe) {
- this.$refs.btn.buy();
- } else {
- this.$refs.btn.$refs.form.submit();
- }
- })
-};
-
-if (typeof window.pretixWidgetCallback !== "undefined") {
- window.pretixWidgetCallback();
-}
-if (window.PretixWidget.build_widgets) {
- window.PretixWidget.buildWidgets();
-}
-
-/* Set a global variable for debugging. In DEBUG mode, siteglobals will be window, otherwise it will be something
- unnamed. */
-siteglobals.pretixwidget_debug = {
- 'Vue': Vue,
- 'widgets': widgetlist,
- 'buttons': buttonlist
-};
diff --git a/src/pretix/static/pretixpresale/scss/_calendar.scss b/src/pretix/static/pretixpresale/scss/_calendar.scss
index 5ff92a785..4e1fc9ae7 100644
--- a/src/pretix/static/pretixpresale/scss/_calendar.scss
+++ b/src/pretix/static/pretixpresale/scss/_calendar.scss
@@ -1,3 +1,35 @@
+.calendar-container:has(.calendar-empty), .event-list:has(.calendar-empty) {
+ position: relative;
+ overflow: hidden;
+
+ .table-responsive, .week-calendar {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ z-index: 0;
+ }
+
+ .calendar-empty {
+ position: relative;
+ z-index: 100;
+ background: rgba(255, 255, 255, 0.9);
+ text-align: center;
+ font-size: $font-size-large;
+ padding: 30px 0;
+
+ .fa {
+ margin-block: 12px;
+ }
+
+ *:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+}
.table-calendar, .week-calendar, .day-calendar {
td, th {
width: 14.29%;
diff --git a/src/pretix/static/pretixpresale/scss/main.scss b/src/pretix/static/pretixpresale/scss/main.scss
index d598c8d92..059130813 100644
--- a/src/pretix/static/pretixpresale/scss/main.scss
+++ b/src/pretix/static/pretixpresale/scss/main.scss
@@ -221,6 +221,11 @@ footer nav .btn-link {
/*border-bottom: 2px solid $brand-primary;*/
font-weight: bold;
}
+ .btn-link {
+ padding: 0;
+ margin-left: 5px;
+ vertical-align: top;
+ }
img {
vertical-align: baseline;
}
@@ -513,7 +518,7 @@ h2 .label {
}
.event-list.full-width-list {
- h3, h4, p {
+ h3, h4, article p {
margin-top: 0;
margin-bottom: 0;
}
@@ -653,6 +658,35 @@ h2 .label {
}
+.helper-position-relative {
+ position: relative;
+}
+.helper-display-block {
+ display: block !important;
+}
+.helper-display-inline {
+ display: inline !important;
+}
+.helper-display-inline-block {
+ display: inline-block !important;
+}
+.helper-display-none-soft {
+ display: none;
+}
+.helper-display-none {
+ display: none !important;
+}
+.helper-width-auto {
+ width: auto;
+}
+.helper-width-100 {
+ width: 100%;
+}
+.helper-space-below {
+ margin-bottom: 10px;
+}
+
+
@import "_iframe.scss";
@import "_a11y.scss";
@import "_print.scss";
diff --git a/src/pretix/static/pretixpresale/scss/widget.scss b/src/pretix/static/pretixpresale/scss/widget.scss
index 9060ea046..5c9905c34 100644
--- a/src/pretix/static/pretixpresale/scss/widget.scss
+++ b/src/pretix/static/pretixpresale/scss/widget.scss
@@ -748,6 +748,13 @@ $table-bg-accent: rgba(128, 128, 128, 0.05);
display: block;
width: 100%;
}
+
+ .pretix-widget-event-list-empty,
+ .pretix-widget-event-week-empty,
+ .pretix-widget-event-calendar-empty {
+ padding: 10px 0;
+ text-align: center;
+ }
}
diff --git a/src/pretix/static/pretixpresale/widget/src/api.ts b/src/pretix/static/pretixpresale/widget/src/api.ts
index 23650186c..a43b8acf9 100644
--- a/src/pretix/static/pretixpresale/widget/src/api.ts
+++ b/src/pretix/static/pretixpresale/widget/src/api.ts
@@ -22,6 +22,7 @@ export interface ProductListResponse {
location?: string
items_by_category?: Category[]
currency?: string
+ currency_places?: number
display_net_prices?: boolean
voucher_explanation_text?: string
error?: string
@@ -37,6 +38,7 @@ export interface ProductListResponse {
events?: EventEntry[]
has_more_events?: boolean
meta_filter_fields?: MetaFilterField[]
+ empty_text?: string
weeks?: DayEntry[][]
date?: string
days?: DayEntry[]
diff --git a/src/pretix/static/pretixpresale/widget/src/components/EventCalendar.vue b/src/pretix/static/pretixpresale/widget/src/components/EventCalendar.vue
index b1ed41aa9..8971e85c4 100644
--- a/src/pretix/static/pretixpresale/widget/src/components/EventCalendar.vue
+++ b/src/pretix/static/pretixpresale/widget/src/components/EventCalendar.vue
@@ -92,7 +92,9 @@ function nextmonth () {
| {{ STRINGS.next_month }} »
//- Calendar
+ .pretix-widget-event-calendar-empty(v-if="!!store.emptyText", v-html="store.emptyText")
table.pretix-widget-event-calendar-table(
+ v-else
:id="id",
tabindex="0",
:aria-labelledby="ariaLabelledby"
diff --git a/src/pretix/static/pretixpresale/widget/src/components/EventCalendarCell.vue b/src/pretix/static/pretixpresale/widget/src/components/EventCalendarCell.vue
index 76bc4db1e..8a7669ab1 100644
--- a/src/pretix/static/pretixpresale/widget/src/components/EventCalendarCell.vue
+++ b/src/pretix/static/pretixpresale/widget/src/components/EventCalendarCell.vue
@@ -114,7 +114,7 @@ td(
)
.pretix-widget-event-calendar-day(v-if="day", :aria-label="dateStr") {{ daynum }}
.pretix-widget-event-calendar-events(v-if="day")
- EventCalendarEvent(v-for="e in day.events", :key="e.event_url", :event="e")
+ EventCalendarEvent(v-for="e in day.events", :key="e.event_url+'-'+(e.subevent||'')", :event="e")
diff --git a/src/pretix/static/pretixpresale/widget/src/components/EventList.vue b/src/pretix/static/pretixpresale/widget/src/components/EventList.vue
index b874675be..198619d8e 100644
--- a/src/pretix/static/pretixpresale/widget/src/components/EventList.vue
+++ b/src/pretix/static/pretixpresale/widget/src/components/EventList.vue
@@ -62,6 +62,8 @@ console.log(store)
:event="event"
)
+ .pretix-widget-event-list-empty(v-if="!store.events.length && store.emptyText", v-html="store.emptyText")
+
p.pretix-widget-event-list-load-more(v-if="store.hasMoreEvents")
button(@click.prevent.stop="loadMore") {{ STRINGS.load_more }}
diff --git a/src/pretix/static/pretixpresale/widget/src/components/EventWeekCalendar.vue b/src/pretix/static/pretixpresale/widget/src/components/EventWeekCalendar.vue
index ad0a2cdeb..63996693d 100644
--- a/src/pretix/static/pretixpresale/widget/src/components/EventWeekCalendar.vue
+++ b/src/pretix/static/pretixpresale/widget/src/components/EventWeekCalendar.vue
@@ -88,7 +88,8 @@ function nextweek () {
| {{ STRINGS.next_week }} »
//- Actual calendar
- .pretix-widget-event-week-table(:id="id", tabindex="0", :aria-label="weekname")
+ .pretix-widget-event-week-empty(v-if="store.emptyText", v-html="store.emptyText")
+ .pretix-widget-event-week-table(v-else, :id="id", tabindex="0", :aria-label="weekname")
.pretix-widget-event-week-col(v-for="d in store.days", :key="d?.date || ''")
EventWeekCell(:day="d", :mobile="mobile")
diff --git a/src/pretix/static/pretixpresale/widget/src/components/Item.vue b/src/pretix/static/pretixpresale/widget/src/components/Item.vue
index 260ee6987..d2c115a57 100644
--- a/src/pretix/static/pretixpresale/widget/src/components/Item.vue
+++ b/src/pretix/static/pretixpresale/widget/src/components/Item.vue
@@ -72,7 +72,7 @@ const pricerange = computed(() => {
STRINGS.price_from,
{
currency: store.currency,
- price: floatformat(props.item.min_price || '0', 2),
+ price: floatformat(props.item.min_price || '0', store.currency_places),
},
true
).replace(
@@ -80,14 +80,14 @@ const pricerange = computed(() => {
`
${store.currency}`
)
} else if (props.item.min_price !== props.item.max_price) {
- return `
${store.currency} ${floatformat(props.item.min_price || '0', 2)} – ${floatformat(props.item.max_price || '0', 2)}`
+ return `
${store.currency} ${floatformat(props.item.min_price || '0', store.currency_places)} – ${floatformat(props.item.max_price || '0', store.currency_places)}`
} else if (props.item.min_price === '0.00' && props.item.max_price === '0.00') {
if (props.item.mandatory_priced_addons) {
return '\xA0' // nbsp, because an empty string would cause the HTML element to collapse
}
return STRINGS.free
} else {
- return `
${store.currency} ${floatformat(props.item.min_price || '0', 2)}`
+ return `
${store.currency} ${floatformat(props.item.min_price || '0', store.currency_places)}`
}
})
diff --git a/src/pretix/static/pretixpresale/widget/src/components/Overlay.vue b/src/pretix/static/pretixpresale/widget/src/components/Overlay.vue
index 4f139fce7..4defa3473 100644
--- a/src/pretix/static/pretixpresale/widget/src/components/Overlay.vue
+++ b/src/pretix/static/pretixpresale/widget/src/components/Overlay.vue
@@ -54,16 +54,16 @@ function lightboxLoaded () {
}
}
-function errorClose (e: Event) {
- const dialog = e.target as HTMLDialogElement
- if (dialog.returnValue === 'continue' && store.overlay.errorUrlAfter) {
- if (store.overlay.errorUrlAfterNewTab) {
- window.open(store.overlay.errorUrlAfter)
- } else {
- store.overlay.frameSrc = store.overlay.errorUrlAfter
- store.overlay.frameLoading = true
- }
+function errorContinue () {
+ if (store.overlay.errorUrlAfterNewTab) {
+ window.open(store.overlay.errorUrlAfter)
+ } else {
+ store.overlay.frameSrc = store.overlay.errorUrlAfter
+ store.overlay.frameLoading = true
}
+}
+
+function errorClose () {
store.overlay.errorMessage = null
store.overlay.errorUrlAfter = null
store.overlay.errorUrlAfterNewTab = false
@@ -168,6 +168,10 @@ onMounted(() => {
onUnmounted(() => {
window.removeEventListener('message', onMessage, false)
})
+
+const vAutofocus = {
+ mounted: (el) => el.focus()
+}
@@ -181,7 +185,7 @@ Teleport(to="body")
strong {{ STRINGS.cancel_blocked }}
.pretix-widget-frame-inner(v-show="store.overlay.frameShown")
form.pretix-widget-frame-close(method="dialog")
- button(ref="closeButton", :aria-label="STRINGS.close_checkout", autofocus)
+ button(ref="closeButton", v-autofocus, :aria-label="STRINGS.close_checkout")
svg(:alt="STRINGS.close", height="16", viewBox="0 0 512 512", width="16", xmlns="http://www.w3.org/2000/svg")
path(fill="#fff", d="M437.5,386.6L306.9,256l130.6-130.6c14.1-14.1,14.1-36.8,0-50.9c-14.1-14.1-36.8-14.1-50.9,0L256,205.1L125.4,74.5 c-14.1-14.1-36.8-14.1-50.9,0c-14.1,14.1-14.1,36.8,0,50.9L205.1,256L74.5,386.6c-14.1,14.1-14.1,36.8,0,50.9 c14.1,14.1,36.8,14.1,50.9,0L256,306.9l130.6,130.6c14.1,14.1,36.8,14.1,50.9,0C451.5,423.4,451.5,400.6,437.5,386.6z")
iframe(
@@ -200,9 +204,9 @@ Teleport(to="body")
form.pretix-widget-alert-box(method="dialog")
p(:id="errorMessageId") {{ store.overlay.errorMessage }}
p
- button(v-if="store.overlay.errorUrlAfter", value="continue", autofocus, :aria-describedby="errorMessageId")
+ button(v-if="store.overlay.errorUrlAfter", v-autofocus, :aria-describedby="errorMessageId", @click="errorContinue")
| {{ STRINGS.continue }}
- button(v-else, autofocus, :aria-describedby="errorMessageId") {{ STRINGS.close }}
+ button(v-else, v-autofocus, :aria-describedby="errorMessageId") {{ STRINGS.close }}
transition(name="bounce")
svg.pretix-widget-alert-icon(v-if="store.overlay.errorMessage", width="64", height="64", viewBox="0 0 1792 1792", xmlns="http://www.w3.org/2000/svg")
path(style="fill:#ffffff;", d="M 599.86438,303.72882 H 1203.5254 V 1503.4576 H 599.86438 Z")
@@ -214,7 +218,7 @@ Teleport(to="body")
path.pretix-widget-primary-color(d="M1152 896q0-106-75-181t-181-75-181 75-75 181 75 181 181 75 181-75 75-181zm512-109v222q0 12-8 23t-20 13l-185 28q-19 54-39 91 35 50 107 138 10 12 10 25t-9 23q-27 37-99 108t-94 71q-12 0-26-9l-138-108q-44 23-91 38-16 136-29 186-7 28-36 28h-222q-14 0-24.5-8.5t-11.5-21.5l-28-184q-49-16-90-37l-141 107q-10 9-25 9-14 0-25-11-126-114-165-168-7-10-7-23 0-12 8-23 15-21 51-66.5t54-70.5q-27-50-41-99l-183-27q-13-2-21-12.5t-8-23.5v-222q0-12 8-23t19-13l186-28q14-46 39-92-40-57-107-138-10-12-10-24 0-10 9-23 26-36 98.5-107.5t94.5-71.5q13 0 26 10l138 107q44-23 91-38 16-136 29-186 7-28 36-28h222q14 0 24.5 8.5t11.5 21.5l28 184q49 16 90 37l142-107q9-9 24-9 13 0 25 10 129 119 165 170 7 8 7 22 0 12-8 23-15 21-51 66.5t-54 70.5q26 50 41 98l183 28q13 2 21 12.5t8 23.5z")
.pretix-widget-lightbox-inner(v-if="store.overlay.lightbox")
form.pretix-widget-lightbox-close(method="dialog")
- button(:aria-label="STRINGS.close", autofocus)
+ button(v-autofocus, :aria-label="STRINGS.close")
svg(:alt="STRINGS.close", height="16", viewBox="0 0 512 512", width="16", xmlns="http://www.w3.org/2000/svg")
path(fill="#fff", d="M437.5,386.6L306.9,256l130.6-130.6c14.1-14.1,14.1-36.8,0-50.9c-14.1-14.1-36.8-14.1-50.9,0L256,205.1L125.4,74.5 c-14.1-14.1-36.8-14.1-50.9,0c-14.1,14.1-14.1,36.8,0,50.9L205.1,256L74.5,386.6c-14.1,14.1-14.1,36.8,0,50.9 c14.1,14.1,36.8,14.1,50.9,0L256,306.9l130.6,130.6c14.1,14.1,36.8,14.1,50.9,0C451.5,423.4,451.5,400.6,437.5,386.6z")
figure.pretix-widget-lightbox-image
diff --git a/src/pretix/static/pretixpresale/widget/src/components/PriceBox.vue b/src/pretix/static/pretixpresale/widget/src/components/PriceBox.vue
index 3ce01162c..ef2733960 100644
--- a/src/pretix/static/pretixpresale/widget/src/components/PriceBox.vue
+++ b/src/pretix/static/pretixpresale/widget/src/components/PriceBox.vue
@@ -25,9 +25,9 @@ const ariaLabelledby = computed(() => `${store.htmlId}-item-label-${props.itemId
const displayPrice = computed(() => {
if (store.displayNetPrices) {
- return floatformat(parseFloat(props.price.net), 2)
+ return floatformat(props.price.net, store.currency_places)
}
- return floatformat(parseFloat(props.price.gross), 2)
+ return floatformat(props.price.gross, store.currency_places)
})
const displayPriceNonlocalized = computed(() => {
@@ -40,15 +40,15 @@ const displayPriceNonlocalized = computed(() => {
const suggestedPriceNonlocalized = computed(() => {
const price = props.suggestedPrice ?? props.price
if (store.displayNetPrices) {
- return parseFloat(price.net).toFixed(2)
+ return parseFloat(price.net).toFixed(store.currency_places)
}
- return parseFloat(price.gross).toFixed(2)
+ return parseFloat(price.gross).toFixed(store.currency_places)
})
// TODO BAD
const originalLine = computed(() => {
if (!props.originalPrice) return ''
- return `${store.currency} ${floatformat(parseFloat(props.originalPrice), 2)}`
+ return `${store.currency} ${floatformat(props.originalPrice, store.currency_places)}`
})
// TODO BAD
@@ -86,7 +86,8 @@ const taxline = computed(() => {
}
})
-const showTaxline = computed(() => props.price.rate !== '0.00' && props.price.gross !== '0.00')
+// rate can either be "0.00" or "0" => parseFloat to check
+const showTaxline = computed(() => Number.parseFloat(props.price.rate) && Number.parseFloat(props.price.gross))
.pretix-widget-pricebox
diff --git a/src/pretix/static/pretixpresale/widget/src/sharedStore.ts b/src/pretix/static/pretixpresale/widget/src/sharedStore.ts
index f43398316..6d1e5662c 100644
--- a/src/pretix/static/pretixpresale/widget/src/sharedStore.ts
+++ b/src/pretix/static/pretixpresale/widget/src/sharedStore.ts
@@ -71,6 +71,7 @@ export function createWidgetStore (config: {
frontpageText: null as string | null,
categories: [] as Category[],
currency: '',
+ currency_places: 2,
displayNetPrices: false,
voucherExplanationText: null as string | null,
displayAddToCart: false,
@@ -94,6 +95,7 @@ export function createWidgetStore (config: {
offset: 0,
appendEvents: false,
metaFilterFields: [] as MetaFilterField[],
+ emptyText: null as string | null,
// UI state
mobile: false,
@@ -269,6 +271,7 @@ export function createWidgetStore (config: {
this.name = data.name ?? null
this.frontpageText = data.frontpage_text ?? null
this.metaFilterFields = data.meta_filter_fields ?? []
+ this.emptyText = data.empty_text ?? null
} else if (data.days !== undefined) {
this.days = data.days
this.date = null
@@ -278,6 +281,7 @@ export function createWidgetStore (config: {
this.name = data.name ?? null
this.frontpageText = data.frontpage_text ?? null
this.metaFilterFields = data.meta_filter_fields ?? []
+ this.emptyText = data.empty_text ?? null
} else if (data.events !== undefined) {
this.events = this.appendEvents && this.events
? this.events.concat(data.events)
@@ -289,6 +293,7 @@ export function createWidgetStore (config: {
this.frontpageText = data.frontpage_text ?? null
this.hasMoreEvents = data.has_more_events ?? false
this.metaFilterFields = data.meta_filter_fields ?? []
+ this.emptyText = data.empty_text ?? null
} else {
this.view = 'event'
this.targetUrl = data.target_url ?? this.targetUrl
@@ -299,6 +304,7 @@ export function createWidgetStore (config: {
this.location = data.location ?? null
this.categories = data.items_by_category ?? []
this.currency = data.currency ?? ''
+ this.currency_places = data.currency_places ?? 2
this.displayNetPrices = data.display_net_prices ?? false
this.voucherExplanationText = data.voucher_explanation_text ?? null
this.error = data.error ?? null
@@ -338,6 +344,7 @@ export function createWidgetStore (config: {
} catch (e) {
this.categories = []
this.currency = ''
+ this.currency_places = 2
if (e instanceof ApiError && e.status === 429) {
this.error = STRINGS.loading_error_429
} else {
@@ -507,10 +514,6 @@ export function createWidgetStore (config: {
// button with subevent but no items
redirectUrl += `${this.subevent}/`
}
- if (this.subevent && !this.cartId) {
- // button with subevent but no items
- redirectUrl += `${this.subevent}/`
- }
redirectUrl += `?iframe=1&locale=${LANG}`
if (this.cartId) {
// ajax to make sure the cart-id is used, even if the cart is currently empty
diff --git a/src/pretix/plugins/paypal/signals.py b/src/pretix/testutils/db.py
similarity index 81%
rename from src/pretix/plugins/paypal/signals.py
rename to src/pretix/testutils/db.py
index 2d4dbd7d0..50692a48d 100644
--- a/src/pretix/plugins/paypal/signals.py
+++ b/src/pretix/testutils/db.py
@@ -19,13 +19,7 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# .
#
-
-from django.dispatch import receiver
-
-from pretix.base.signals import register_payment_providers
-
-
-@receiver(register_payment_providers, dispatch_uid="payment_paypal")
-def register_payment_provider(sender, **kwargs):
- from .payment import Paypal
- return Paypal
+def readonly_db(execute, sql, params, many, context):
+ if not sql.lower().startswith("select"):
+ raise Exception(f"Should not write anything to the database, but detected query: {sql}")
+ return execute(sql, params, many, context)
diff --git a/src/tests/api/conftest.py b/src/tests/api/conftest.py
index a4d617b22..01179e669 100644
--- a/src/tests/api/conftest.py
+++ b/src/tests/api/conftest.py
@@ -61,6 +61,15 @@ def meta_prop(organizer):
return organizer.meta_properties.create(name="type", default="Concert")
+@pytest.fixture
+@scopes_disabled()
+def meta_prop_choices(organizer):
+ return organizer.meta_properties.create(name="department", default="A", choices=[
+ {"key": "A", "label": {"en": "Group A"}},
+ {"key": "B", "label": {"en": "Group B"}},
+ ])
+
+
@pytest.fixture
@scopes_disabled()
def event(organizer, meta_prop):
diff --git a/src/tests/api/test_checkinrpc.py b/src/tests/api/test_checkinrpc.py
index 6ea5066cb..da27c78ad 100644
--- a/src/tests/api/test_checkinrpc.py
+++ b/src/tests/api/test_checkinrpc.py
@@ -25,6 +25,7 @@ from unittest import mock
import pytest
from django.core.files.base import ContentFile
+from django.db import connection
from django.utils.timezone import now
from django_countries.fields import Country
from django_scopes import scopes_disabled
@@ -36,6 +37,7 @@ from pretix.api.serializers.item import QuestionSerializer
from pretix.base.models import (
Checkin, InvoiceAddress, Item, Order, OrderPosition, ReusableMedium,
)
+from pretix.testutils.db import readonly_db
# Lots of this code is overlapping with test_checkin.py, and some of it is arguably redundant since it's triggering
# the same backend code paths (for now). However, this is SUCH a critical part of pretix that we don't want to take
@@ -1739,3 +1741,41 @@ def test_exchange_create_gift_card(token_client, organizer, clist, event, order,
with scopes_disabled():
rm = ReusableMedium.objects.get(identifier="0412345")
assert rm.linked_giftcard.currency == "EUR"
+
+
+@pytest.mark.django_db
+def test_simulate(token_client, organizer, clist, event, order):
+ with scopes_disabled():
+ p = order.positions.first()
+ with connection.execute_wrapper(readonly_db):
+ resp = _redeem(token_client, organizer, clist, p.secret, {"simulate": True})
+ assert resp.status_code == 201
+ assert resp.data['status'] == 'ok'
+ with scopes_disabled():
+ assert not p.checkins.exists()
+
+
+@pytest.mark.django_db
+def test_simulate_no_exchange(token_client, organizer, clist, event, order, item):
+ organizer.settings.reusable_media_type_nfc_uid = True
+ item.media_type = "nfc_uid"
+ item.media_policy = Item.MEDIA_POLICY_NEW
+ item.save()
+ with scopes_disabled():
+ rm = ReusableMedium.objects.create(
+ type="nfc_uid",
+ identifier="12345678",
+ organizer=organizer,
+ )
+ with connection.execute_wrapper(readonly_db):
+ resp = _redeem(token_client, organizer, clist, "z3fsn8jyufm5kpk768q69gkbyr5f4h6w", {
+ "source_type": "barcode",
+ "exchange_medium_type": "nfc_uid",
+ "exchange_medium_identifier": "12345678",
+ "simulate": True,
+ })
+ assert resp.status_code == 400
+ assert resp.data['status'] == 'error'
+ assert resp.data['reason'] == 'error'
+ with scopes_disabled():
+ assert not rm.linked_orderpositions.exists()
diff --git a/src/tests/api/test_deviceauth.py b/src/tests/api/test_deviceauth.py
index dd29809b7..e1f29c2fb 100644
--- a/src/tests/api/test_deviceauth.py
+++ b/src/tests/api/test_deviceauth.py
@@ -20,13 +20,16 @@
# .
#
import base64
+from datetime import datetime, timezone
import pytest
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from django_scopes import scopes_disabled
+from freezegun import freeze_time
from pretix.base.models import Device
+from pretix.base.models.devices import DeviceLastSeen
@pytest.fixture
@@ -386,3 +389,26 @@ def test_device_info_key_sets(device_client, device: Device):
base64.b64decode(ks['diversification_key']),
padding.PKCS1v15()
)
+
+
+@pytest.mark.django_db
+def test_update_last_seen(device_client, device: Device):
+ assert not DeviceLastSeen.objects.exists()
+
+ with freeze_time("2020-01-10T14:30:00+00:00"):
+ resp = device_client.get('/api/v1/device/info')
+ assert resp.status_code == 200
+ assert device.last_seen.last_seen == datetime(2020, 1, 10, 14, 30, tzinfo=timezone.utc)
+
+ with freeze_time("2020-01-10T14:30:05+00:00"):
+ resp = device_client.get('/api/v1/device/info')
+ assert resp.status_code == 200
+ # No update, interal too short
+ device.last_seen.refresh_from_db()
+ assert device.last_seen.last_seen == datetime(2020, 1, 10, 14, 30, tzinfo=timezone.utc)
+
+ with freeze_time("2020-01-10T14:30:30+00:00"):
+ resp = device_client.get('/api/v1/device/info')
+ assert resp.status_code == 200
+ device.last_seen.refresh_from_db()
+ assert device.last_seen.last_seen == datetime(2020, 1, 10, 14, 30, 30, tzinfo=timezone.utc)
diff --git a/src/tests/api/test_event_meta_properties.py b/src/tests/api/test_event_meta_properties.py
new file mode 100644
index 000000000..173a198a3
--- /dev/null
+++ b/src/tests/api/test_event_meta_properties.py
@@ -0,0 +1,229 @@
+#
+# This file is part of pretix (Community Edition).
+#
+# Copyright (C) 2014-2020 Raphael Michel and contributors
+# Copyright (C) 2020-today pretix 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 .
+#
+# 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
+# .
+#
+import pytest
+from django_scopes import scopes_disabled
+from i18nfield.strings import LazyI18nString
+
+
+@pytest.fixture
+def event_meta_property(organizer):
+ return organizer.meta_properties.create(
+ name="Color",
+ default="Red",
+ required=False,
+ choices=[
+ {
+ "key": "Red",
+ "label": LazyI18nString("Rot"),
+ "DELETE": False,
+ "ORDER": 1,
+ }
+ ],
+ )
+
+
+TEST_TYPE_RES = {
+ "name": "Color",
+ "default": "Red",
+ "required": False,
+ "choices": [{"key": "Red", "label": {"en": "Rot"}}],
+ 'filter_allowed': True,
+ 'filter_public': False,
+ 'protected': False,
+ 'public_label': None,
+}
+
+
+@pytest.mark.django_db
+def test_meta_property_list(token_client, organizer, event_meta_property):
+ res = dict(TEST_TYPE_RES)
+
+ resp = token_client.get('/api/v1/organizers/{}/event_meta_properties/'.format(organizer.slug))
+ assert resp.status_code == 200
+ event_meta_property.refresh_from_db()
+ res["id"] = event_meta_property.pk
+ assert res in resp.data['results']
+ assert len(resp.data['results']) == 1
+
+
+@pytest.mark.django_db
+def test_meta_property_detail(token_client, organizer, event_meta_property):
+ res = TEST_TYPE_RES
+ resp = token_client.get('/api/v1/organizers/{}/event_meta_properties/{}/'.format(organizer.slug, event_meta_property.pk))
+ assert resp.status_code == 200
+ event_meta_property.refresh_from_db()
+ res["id"] = event_meta_property.pk
+ assert res == resp.data
+
+
+@pytest.mark.django_db
+def test_meta_property_create(token_client, organizer):
+ url = '/api/v1/organizers/{}/event_meta_properties/'.format(organizer.slug)
+ resp = token_client.post(
+ url,
+ format='json',
+ data={
+ "name": "Color",
+ "default": "",
+ "required": False,
+ "choices": [
+ {"key": {"foo": "bar"}},
+ "blabla",
+ {"label": "Green"},
+ {"key": "g", "label": "Green", "foo": "bar"},
+ ],
+ }
+ )
+ assert resp.status_code == 400
+ assert str(resp.data["choices"][0][0]) == "Meta property value options must have a key of type string."
+ assert str(resp.data["choices"][1][0]) == "Meta property value options must be a dict."
+ assert str(resp.data["choices"][2][0]) == "Meta property value options must have a key of type string."
+ assert str(resp.data["choices"][3][0]) == "Meta property value options may only have a key and optionally a label."
+
+ resp = token_client.post(
+ url,
+ format='json',
+ data={
+ "name": "Color",
+ "default": "Red",
+ "required": False,
+ "choices": {"key": "r", "label": "Red"},
+ }
+ )
+ assert resp.status_code == 400
+ assert str(resp.data["choices"][0]) == 'Expected a list of items but got type "dict".'
+
+ resp = token_client.post(
+ url,
+ format='json',
+ data={
+ "name": "Color",
+ "default": "r",
+ "required": False,
+ "choices": [
+ {"key": "r", "label": {"en": "Red"}},
+ {"key": "r", "label": {"en": "Razzmatazz"}},
+ ],
+ }
+ )
+ assert resp.status_code == 400
+ assert str(resp.data["choices"][0]) == "The key for each meta property value option must be unique."
+
+ choices = [
+ {"key": "r", "label": "Red"},
+ {"key": "g", "label": "Green"},
+ {"key": "b", "label": "Blue"},
+ ]
+ resp = token_client.post(
+ url,
+ format='json',
+ data={
+ "name": "Color",
+ "default": "k",
+ "required": False,
+ "choices": choices,
+ }
+ )
+ assert resp.status_code == 400
+ assert str(resp.data["non_field_errors"][0]) == "You cannot set a default value that is not a valid value."
+
+ resp = token_client.post(
+ url,
+ format='json',
+ data={
+ "name": "Color",
+ "default": "r",
+ "required": False,
+ "choices": choices,
+ }
+ )
+ assert resp.status_code == 201
+ with scopes_disabled():
+ event_meta_property = organizer.meta_properties.get(id=resp.data['id'])
+ assert event_meta_property.name == "Color"
+ assert event_meta_property.default == "r"
+ assert event_meta_property.choices == choices
+ assert not event_meta_property.required
+ assert len(organizer.meta_properties.all()) == 1
+
+
+@pytest.mark.django_db
+def test_meta_property_patch(token_client, organizer, event_meta_property):
+ url = '/api/v1/organizers/{}/event_meta_properties/{}/'.format(organizer.slug, event_meta_property.pk)
+ resp = token_client.patch(
+ url,
+ format='json',
+ data={
+ # existing default is not in choices
+ "choices": [{'key': 'k', 'label': 'Black'}],
+ }
+ )
+ assert resp.status_code == 400
+ assert str(resp.data["non_field_errors"][0]) == "You cannot set a default value that is not a valid value."
+
+ resp = token_client.patch(
+ "/api/v1/organizers/{}/event_meta_properties/{}/"
+ .format(organizer.slug, event_meta_property.pk),
+ format="json",
+ data={
+ "choices": [
+ {"key": "r", "label": ["wrong"]},
+ {"key": "g", "label": {"de": {"de": 123, "en": "wrong"}}}
+ ],
+ }
+ )
+ assert resp.status_code == 400
+ assert str(resp.data["choices"][0]["label"][0]) == "Invalid data type."
+ assert str(resp.data["choices"][1]["label"][0]) == "All entries must be strings."
+
+ resp = token_client.patch(
+ url,
+ format='json',
+ data={
+ "choices": [],
+ }
+ )
+ assert resp.status_code == 200
+ event_meta_property.refresh_from_db()
+ assert event_meta_property.choices is None
+
+ resp = token_client.patch(
+ url,
+ format='json',
+ data={
+ "required": True,
+ "choices": None,
+ }
+ )
+ assert resp.status_code == 200
+ event_meta_property.refresh_from_db()
+ assert event_meta_property.required
+ assert event_meta_property.choices is None
+
+
+@pytest.mark.django_db
+def test_meta_property_delete(token_client, organizer, event_meta_property):
+ resp = token_client.delete(
+ '/api/v1/organizers/{}/event_meta_properties/{}/'.format(organizer.slug, event_meta_property.pk),
+ )
+ assert resp.status_code == 204
+ assert len(organizer.meta_properties.all()) == 0
diff --git a/src/tests/api/test_events.py b/src/tests/api/test_events.py
index 9838a3d50..fc93ea944 100644
--- a/src/tests/api/test_events.py
+++ b/src/tests/api/test_events.py
@@ -703,7 +703,7 @@ def test_event_delete_with_clone(token_client, organizer, event, meta_prop):
@pytest.mark.django_db
-def test_event_update(token_client, organizer, event, item, meta_prop):
+def test_event_update(token_client, team, organizer, event, item, meta_prop, meta_prop_choices):
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
{
@@ -782,6 +782,26 @@ def test_event_update(token_client, organizer, event, item, meta_prop):
property__name=meta_prop.name, value="Workshop"
).exists()
+ resp = token_client.patch(
+ '/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
+ {
+ "meta_data": {
+ meta_prop.name: "Workshop",
+ meta_prop_choices.name: "B"
+ }
+ },
+ format='json'
+ )
+ assert resp.status_code == 200
+ assert resp.data["meta_data"] == {
+ meta_prop.name: "Workshop",
+ meta_prop_choices.name: "B"
+ }
+ with scopes_disabled():
+ assert organizer.events.get(slug=resp.data['slug']).meta_values.filter(
+ property__name=meta_prop_choices.name, value="B"
+ ).exists()
+
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
{
@@ -796,6 +816,51 @@ def test_event_update(token_client, organizer, event, item, meta_prop):
property__name=meta_prop.name
).exists()
+ resp = token_client.patch(
+ '/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
+ {
+ "meta_data": {
+ meta_prop_choices.name: "invalid"
+ }
+ },
+ format='json'
+ )
+ assert resp.status_code == 400
+ assert resp.content.decode() == '{"meta_data":["Meta data property \'department\' does not allow value \'invalid\'."]}'
+
+ meta_prop_choices.protected = True
+ meta_prop_choices.save()
+ team.all_organizer_permissions = False
+ team.limit_organizer_permissions = {}
+ team.save()
+ resp = token_client.patch(
+ '/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
+ {
+ "meta_data": {
+ meta_prop_choices.name: "A"
+ }
+ },
+ format='json'
+ )
+ assert resp.status_code == 200 # silently ignored
+ assert resp.data["meta_data"] == {}
+
+ team.all_organizer_permissions = True
+ team.save()
+ resp = token_client.patch(
+ '/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
+ {
+ "meta_data": {
+ meta_prop_choices.name: "A"
+ }
+ },
+ format='json'
+ )
+ assert resp.status_code == 200
+ assert resp.data["meta_data"] == {
+ meta_prop_choices.name: "A"
+ }
+
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
{
@@ -1434,8 +1499,12 @@ def test_get_event_settings(token_client, organizer, event):
'/api/v1/organizers/{}/events/{}/settings/'.format(organizer.slug, event.slug),
)
assert resp.status_code == 200
- assert resp.data['imprint_url'] == "https://example.org"
- assert resp.data['contact_url'] == "https://example.org/contact"
+ assert resp.data['imprint_url'] == {
+ "en": "https://example.org",
+ }
+ assert resp.data['contact_url'] == {
+ "en": "https://example.org/contact",
+ }
assert resp.data['seating_allow_blocked_seats_for_channel'] == []
resp = token_client.get(
@@ -1443,7 +1512,9 @@ def test_get_event_settings(token_client, organizer, event):
)
assert resp.status_code == 200
assert resp.data['imprint_url'] == {
- "value": "https://example.org",
+ "value": {
+ "en": "https://example.org",
+ },
"label": "Imprint URL",
"help_text": "This should point e.g. to a part of your website that has your contact details and legal "
"information.",
@@ -1478,8 +1549,12 @@ def test_patch_event_settings(token_client, organizer, event, team):
format='json'
)
assert resp.status_code == 200
- assert resp.data['contact_url'] == "https://example.com/contact"
- assert resp.data['imprint_url'] == "https://example.com"
+ assert resp.data['contact_url'] == {
+ "en": "https://example.com/contact",
+ }
+ assert resp.data['imprint_url'] == {
+ "en": "https://example.com",
+ }
assert resp.data['seating_allow_blocked_seats_for_channel'] == ['web']
assert not resp.data['reusable_media_active']
event.settings.flush()
@@ -1542,8 +1617,12 @@ def test_patch_event_settings(token_client, organizer, event, team):
format='json'
)
assert resp.status_code == 200
- assert resp.data['contact_url'] == "https://example.org/contact"
- assert resp.data['imprint_url'] == "https://example.org"
+ assert resp.data['contact_url'] == {
+ "en": "https://example.org/contact",
+ }
+ assert resp.data['imprint_url'] == {
+ "en": "https://example.org",
+ }
event.settings.flush()
assert event.settings.contact_url == 'https://example.org/contact'
assert event.settings.imprint_url == 'https://example.org'
diff --git a/src/tests/api/test_items.py b/src/tests/api/test_items.py
index 7b61601f0..aef2ef31f 100644
--- a/src/tests/api/test_items.py
+++ b/src/tests/api/test_items.py
@@ -2479,6 +2479,7 @@ TEST_QUESTION_RES = {
"valid_datetime_min": None,
"valid_datetime_max": None,
"valid_file_portrait": False,
+ "valid_string_length_min": None,
"valid_string_length_max": None,
"help_text": {"en": "This is an example question"},
"options": [
diff --git a/src/tests/api/test_oauth.py b/src/tests/api/test_oauth.py
index 082ad6e5e..71ce92ee7 100644
--- a/src/tests/api/test_oauth.py
+++ b/src/tests/api/test_oauth.py
@@ -70,14 +70,27 @@ def admin_user(admin_team):
@pytest.fixture
-def application():
+def app_developer():
+ return User.objects.create_user('app-developer@example.org', 'app-developer')
+
+
+@pytest.fixture
+def client2():
+ # We need a second test client instance to log in as the app developer user
+ from django.test import Client
+ return Client()
+
+
+@pytest.fixture
+def application(app_developer):
secret = get_random_string(32)
a = OAuthApplication.objects.create(
name="pretalx",
redirect_uris="https://pretalx.com",
client_type='confidential',
client_secret=secret,
- authorization_grant_type='authorization-code'
+ authorization_grant_type='authorization-code',
+ user=app_developer,
)
a._cached_secret = secret
a.save()
@@ -703,6 +716,47 @@ def test_token_revoke_access_token(client, admin_user, organizer, application: O
assert list(grant.organizers.all()) == [organizer]
+@pytest.mark.django_db
+def test_token_app_disabled(client, client2, admin_user, organizer, application: OAuthApplication, app_developer):
+ client.login(email='dummy@dummy.dummy', password='dummy')
+ session = client.session
+ session['pretix_auth_login_time'] = int(time.time())
+ session.save()
+ resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code' % (
+ application.client_id, quote(application.redirect_uris)
+ ))
+ assert resp.status_code == 200
+ resp = client.post('/api/v1/oauth/authorize', data={
+ 'organizers': str(organizer.pk),
+ 'redirect_uri': application.redirect_uris,
+ 'scope': 'read write',
+ 'client_id': application.client_id,
+ 'response_type': 'code',
+ 'allow': 'Authorize',
+ })
+ assert resp.status_code == 302
+ assert resp['Location'].startswith('https://pretalx.com?code=')
+ code = resp['Location'].split("=")[1]
+ client.logout()
+ resp = client.post('/api/v1/oauth/token', data={
+ 'code': code,
+ 'redirect_uri': application.redirect_uris,
+ 'grant_type': 'authorization_code',
+ }, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
+ ('%s:%s' % (application.client_id, application._cached_secret)).encode()).decode())
+ assert resp.status_code == 200
+ data = json.loads(resp.content.decode())
+ access_token = data['access_token']
+ resp = client.get('/api/v1/organizers/dummy/events/', HTTP_AUTHORIZATION='Bearer %s' % access_token)
+ assert resp.status_code == 200
+
+ client2.login(email='app-developer@example.org', password='app-developer')
+ client2.post(f'/control/settings/oauth/apps/{application.pk}/disable', {})
+
+ resp = client.get('/api/v1/organizers/dummy/events/', HTTP_AUTHORIZATION='Bearer %s' % access_token)
+ assert resp.status_code == 401
+
+
@pytest.mark.django_db
def test_user_revoke(client, admin_user, organizer, application: OAuthApplication):
client.login(email='dummy@dummy.dummy', password='dummy')
diff --git a/src/tests/api/test_organizers.py b/src/tests/api/test_organizers.py
index 1ec00a5ad..0c95fe007 100644
--- a/src/tests/api/test_organizers.py
+++ b/src/tests/api/test_organizers.py
@@ -25,6 +25,7 @@ from datetime import datetime
import pytest
from django.core.files.base import ContentFile
from django_scopes import scopes_disabled
+from i18nfield.strings import LazyI18nString
from tests.const import SAMPLE_PNG
TEST_ORGANIZER_RES = {
@@ -165,9 +166,11 @@ def test_patch_settings(token_client, organizer):
format='json'
)
assert resp.status_code == 200
- assert resp.data['contact_url'] == 'https://example.org/contact'
+ assert resp.data['contact_url'] == {
+ 'en': 'https://example.org/contact',
+ }
organizer.settings.flush()
- assert organizer.settings.contact_url == 'https://example.org/contact'
+ assert organizer.settings.contact_url == LazyI18nString('https://example.org/contact')
resp = token_client.patch(
'/api/v1/organizers/{}/settings/'.format(organizer.slug),
diff --git a/src/tests/api/test_permissions.py b/src/tests/api/test_permissions.py
index d25ec2d1a..d1b1804a3 100644
--- a/src/tests/api/test_permissions.py
+++ b/src/tests/api/test_permissions.py
@@ -223,6 +223,8 @@ org_permission_sub_urls = [
('post', 'organizer.customers:write', 'customers/1/anonymize/', 404),
('put', 'organizer.customers:write', 'customers/1/', 404),
('delete', 'organizer.customers:write', 'customers/1/', 404),
+ ('post', 'organizer.settings.general:write', 'event_meta_properties/', 400),
+ ('patch', 'organizer.settings.general:write', 'event_meta_properties/0/', 404),
('get', 'organizer.customers:read', 'memberships/', 200),
('post', 'organizer.customers:write', 'memberships/', 400),
('get', 'organizer.customers:read', 'memberships/1/', 404),
diff --git a/src/tests/api/test_taxrules.py b/src/tests/api/test_taxrules.py
index 383272e47..7c47e6ece 100644
--- a/src/tests/api/test_taxrules.py
+++ b/src/tests/api/test_taxrules.py
@@ -61,17 +61,21 @@ def test_rule_detail(token_client, organizer, event, taxrule):
@pytest.mark.django_db
def test_rule_create(token_client, organizer, event):
- resp = token_client.post(
- '/api/v1/organizers/{}/events/{}/taxrules/'.format(organizer.slug, event.slug),
- {
- "name": {"en": "VAT", "de": "MwSt"},
- "rate": "19.00",
- "price_includes_tax": True,
- "eu_reverse_charge": False,
- "home_country": "DE"
- },
- format='json'
- )
+ url = '/api/v1/organizers/{}/events/{}/taxrules/'.format(organizer.slug, event.slug)
+ payload = {
+ "name": {"en": "VAT", "de": "MwSt"},
+ "rate": "19.00",
+ "price_includes_tax": True,
+ "eu_reverse_charge": False,
+ "home_country": "DE",
+ "default": False,
+ }
+ # fail as only one rule exists and must be default
+ resp = token_client.post(url, payload, format='json')
+ assert resp.status_code == 400
+
+ del payload["default"]
+ resp = token_client.post(url, payload, format='json')
assert resp.status_code == 201
rule = TaxRule.objects.get(pk=resp.data['id'])
assert rule.name.data == {"en": "VAT", "de": "MwSt"}
@@ -79,6 +83,7 @@ def test_rule_create(token_client, organizer, event):
assert rule.price_includes_tax is True
assert rule.eu_reverse_charge is False
assert str(rule.home_country) == "DE"
+ assert rule.default is True
@pytest.mark.django_db
diff --git a/src/tests/api/test_teams.py b/src/tests/api/test_teams.py
index dca30e609..c0cea19fa 100644
--- a/src/tests/api/test_teams.py
+++ b/src/tests/api/test_teams.py
@@ -83,6 +83,7 @@ def test_team_detail(token_client, organizer, event, second_team):
TEST_TEAM_CREATE_PAYLOAD = {
"name": "Foobar",
"limit_events": ["dummy"],
+ "can_view_orders": True,
}
diff --git a/src/tests/api/test_vouchers.py b/src/tests/api/test_vouchers.py
index c06079b5e..8061d1c4a 100644
--- a/src/tests/api/test_vouchers.py
+++ b/src/tests/api/test_vouchers.py
@@ -115,6 +115,15 @@ def test_voucher_list(token_client, organizer, event, voucher, item, quota, sube
)
assert [] == resp.data['results']
+ resp = token_client.get(
+ '/api/v1/organizers/{}/events/{}/vouchers/?search={}'.format(organizer.slug, event.slug, voucher.code[:3])
+ )
+ assert [res] == resp.data['results']
+ resp = token_client.get(
+ '/api/v1/organizers/{}/events/{}/vouchers/?code=RAvRcnbDehWD59oywV5x'.format(organizer.slug, event.slug)
+ )
+ assert [] == resp.data['results']
+
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/vouchers/?max_usages=1'.format(organizer.slug, event.slug)
)
diff --git a/src/tests/base/test_mail.py b/src/tests/base/test_mail.py
index 9a5a9e66a..e685fdedc 100644
--- a/src/tests/base/test_mail.py
+++ b/src/tests/base/test_mail.py
@@ -34,6 +34,7 @@
import datetime
import os
+import quopri
import re
import socket
from contextlib import contextmanager
@@ -316,8 +317,10 @@ def _extract_html(mail):
if "multipart/related" in mimetype:
for sp in content._payload:
if isinstance(sp, MIMEText):
- return sp._payload
- break
+ if sp.get('content-transfer-encoding').startswith('quoted-printable'):
+ return quopri.decodestring(sp.get_payload()).decode()
+ else:
+ return sp.get_payload()
elif "text/html" in mimetype:
return content
@@ -711,3 +714,29 @@ def test_send_mail_private_ip(res, use_ssl, allow_private_networks, env):
assert m.status == OutgoingMail.STATUS_SENT
else:
assert m.status == OutgoingMail.STATUS_FAILED
+
+
+@pytest.mark.django_db
+def test_mail_encoding(env, mailoutbox):
+ m = OutgoingMail.objects.create(
+ to=['recipient@example.com'],
+ subject='Test',
+ body_plain='Test öäü testtesttest',
+ body_html='Test ÖÄÜ',
+ sender='sender@example.com',
+ )
+ assert m.status == OutgoingMail.STATUS_QUEUED
+ mail_send_task.apply(kwargs={
+ 'outgoing_mail': m.pk,
+ }, max_retries=0)
+ m.refresh_from_db()
+ assert m.status == OutgoingMail.STATUS_SENT
+
+ assert len(mailoutbox) == 1
+ mail_plain = mailoutbox[0].message().as_string()
+
+ assert "ä" not in mail_plain
+ assert "Ä" not in mail_plain
+ assert "Test =C3=B6=C3=A4=C3=BC" in mail_plain
+ assert "Test =C3=96=C3=84=C3=9C" in mail_plain
+ assert max(len(x) for x in mail_plain.splitlines()) <= 76
diff --git a/src/tests/base/test_modelimport_orders.py b/src/tests/base/test_modelimport_orders.py
index 1c51ab219..aa0f5eac6 100644
--- a/src/tests/base/test_modelimport_orders.py
+++ b/src/tests/base/test_modelimport_orders.py
@@ -43,7 +43,7 @@ def event():
o = Organizer.objects.create(name='Dummy', slug='dummy')
event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
- date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
+ date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal2'
)
return event
diff --git a/src/tests/base/test_modelimport_vouchers.py b/src/tests/base/test_modelimport_vouchers.py
index 1c0e6cc9b..ca95929a3 100644
--- a/src/tests/base/test_modelimport_vouchers.py
+++ b/src/tests/base/test_modelimport_vouchers.py
@@ -37,7 +37,7 @@ def event():
o = Organizer.objects.create(name='Dummy', slug='dummy')
event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
- date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
+ date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal2'
)
return event
diff --git a/src/tests/base/test_models.py b/src/tests/base/test_models.py
index 6f2359db1..349c8c37b 100644
--- a/src/tests/base/test_models.py
+++ b/src/tests/base/test_models.py
@@ -473,7 +473,7 @@ class QuotaTestCase(BaseQuotaTestCase):
@classscope(attr='o')
def test_waitinglist_auto_disable(self):
self.event.settings.waiting_list_auto_disable = RelativeDateWrapper(
- RelativeDate(days=0, time=None, base_date_name='date_from', minutes=20, is_after=True)
+ RelativeDate(days=0, time=None, base_date_name='event__date_from', minutes=20, is_after=True)
)
self.quota.items.add(self.item1)
self.quota.size = 1
@@ -484,7 +484,7 @@ class QuotaTestCase(BaseQuotaTestCase):
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0))
self.assertEqual(self.item1.check_quotas(count_waitinglist=False), (Quota.AVAILABILITY_OK, 1))
self.event.settings.waiting_list_auto_disable = RelativeDateWrapper(
- RelativeDate(days=0, time=None, base_date_name='date_from', minutes=20, is_after=False)
+ RelativeDate(days=0, time=None, base_date_name='event__date_from', minutes=20, is_after=False)
)
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
self.assertEqual(self.item1.check_quotas(count_waitinglist=False), (Quota.AVAILABILITY_OK, 1))
@@ -1192,7 +1192,7 @@ class OrderTestCase(BaseQuotaTestCase):
self.op2.subevent = se2
self.op2.save()
self.event.settings.set('payment_term_last', RelativeDateWrapper(
- RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
+ RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
self.order.status = Order.STATUS_EXPIRED
@@ -1347,7 +1347,7 @@ class OrderTestCase(BaseQuotaTestCase):
self.op2.subevent = se2
self.op2.save()
self.event.settings.set('last_order_modification_date', RelativeDateWrapper(
- RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
+ RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
assert self.order.can_modify_answers
self.op2.subevent = se3
@@ -1363,7 +1363,7 @@ class OrderTestCase(BaseQuotaTestCase):
self.event.date_from = datetime.datetime(2017, 5, 3, 12, 0, 0, tzinfo=datetime.timezone.utc)
self.event.save()
self.event.settings.set('payment_term_last', RelativeDateWrapper(
- RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
+ RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
assert self.order.payment_term_last == datetime.datetime(2017, 5, 1, 23, 59, 59, tzinfo=datetime.timezone.utc)
@@ -1379,7 +1379,7 @@ class OrderTestCase(BaseQuotaTestCase):
self.op2.subevent = se2
self.op2.save()
self.event.settings.set('payment_term_last', RelativeDateWrapper(
- RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
+ RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
assert self.order.payment_term_last > now()
self.op2.subevent = se3
@@ -1395,7 +1395,7 @@ class OrderTestCase(BaseQuotaTestCase):
self.event.date_from = datetime.datetime(2017, 5, 3, 12, 0, 0, tzinfo=datetime.timezone.utc)
self.event.save()
self.event.settings.set('ticket_download_date', RelativeDateWrapper(
- RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
+ RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
assert self.order.ticket_download_date == datetime.datetime(2017, 5, 1, 12, 0, 0, tzinfo=datetime.timezone.utc)
@@ -1411,7 +1411,7 @@ class OrderTestCase(BaseQuotaTestCase):
self.op2.subevent = se2
self.op2.save()
self.event.settings.set('ticket_download_date', RelativeDateWrapper(
- RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
+ RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
assert self.order.ticket_download_date > now()
self.op2.subevent = se3
@@ -1582,13 +1582,13 @@ class OrderTestCase(BaseQuotaTestCase):
assert self.order.user_cancel_deadline is None
self.event.settings.set('cancel_allow_user_until', RelativeDateWrapper(
- RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='date_from', minutes=None)
+ RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='event__date_from', minutes=None)
))
self.order = Order.objects.get(pk=self.order.pk)
assert self.order.user_cancel_deadline > now()
assert self.order.user_cancel_allowed
self.event.settings.set('cancel_allow_user_until', RelativeDateWrapper(
- RelativeDate(days=4, time=datetime.time(14, 0, 0), base_date_name='date_from', minutes=None)
+ RelativeDate(days=4, time=datetime.time(14, 0, 0), base_date_name='event__date_from', minutes=None)
))
self.order = Order.objects.get(pk=self.order.pk)
assert self.order.user_cancel_deadline < now()
@@ -1607,7 +1607,7 @@ class OrderTestCase(BaseQuotaTestCase):
self.op2.save()
self.event.settings.set('cancel_allow_user_until', RelativeDateWrapper(
- RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='date_from', minutes=None)
+ RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='event__date_from', minutes=None)
))
self.order = Order.objects.get(pk=self.order.pk)
assert self.order.user_cancel_deadline < now()
@@ -2018,13 +2018,13 @@ class OrderTestCase(BaseQuotaTestCase):
assert self.order.user_change_deadline is None
self.event.settings.set('change_allow_user_until', RelativeDateWrapper(
- RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='date_from', minutes=None)
+ RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='event__date_from', minutes=None)
))
self.order = Order.objects.get(pk=self.order.pk)
assert self.order.user_change_deadline > now()
assert self.order.user_change_allowed
self.event.settings.set('change_allow_user_until', RelativeDateWrapper(
- RelativeDate(days=4, time=datetime.time(14, 0, 0), base_date_name='date_from', minutes=None)
+ RelativeDate(days=4, time=datetime.time(14, 0, 0), base_date_name='event__date_from', minutes=None)
))
self.order = Order.objects.get(pk=self.order.pk)
assert self.order.user_change_deadline < now()
@@ -2046,7 +2046,7 @@ class OrderTestCase(BaseQuotaTestCase):
self.op2.save()
self.event.settings.set('change_allow_user_until', RelativeDateWrapper(
- RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='date_from', minutes=None)
+ RelativeDate(days=2, time=datetime.time(14, 0, 0), base_date_name='event__date_from', minutes=None)
))
self.order = Order.objects.get(pk=self.order.pk)
assert self.order.user_change_deadline < now()
@@ -2970,8 +2970,9 @@ class SeatingTestCase(TestCase):
@pytest.mark.django_db
@pytest.mark.parametrize("qtype,answer,expected", [
- (Question.TYPE_STRING, "a", "a"),
- (Question.TYPE_TEXT, "v", "v"),
+ (Question.TYPE_STRING, "aaa", "aaa"),
+ (Question.TYPE_STRING, "a", ValidationError),
+ (Question.TYPE_TEXT, "vvv", "vvv"),
(Question.TYPE_TEXT, "waaaaay tooooo long", ValidationError),
(Question.TYPE_NUMBER, "0.9", ValidationError),
(Question.TYPE_NUMBER, "1", Decimal("1")),
@@ -3025,6 +3026,7 @@ def test_question_answer_validation(qtype, answer, expected):
valid_datetime_max=datetime.datetime(2018, 1, 16, 16, 0, 0, tzinfo=tzoffset(None, 3600)),
valid_number_min=Decimal('1'),
valid_number_max=Decimal('100'),
+ valid_string_length_min=3,
valid_string_length_max=8,
)
if isinstance(expected, type) and issubclass(expected, Exception):
diff --git a/src/tests/base/test_orders.py b/src/tests/base/test_orders.py
index 821a4fc5f..4e91e26f1 100644
--- a/src/tests/base/test_orders.py
+++ b/src/tests/base/test_orders.py
@@ -204,7 +204,7 @@ def test_expiry_last_relative(event):
event.date_from = now() + timedelta(days=5)
event.save()
event.settings.set('payment_term_last', RelativeDateWrapper(
- RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
+ RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
order = _create_order(event, email='dummy@example.org', positions=[],
now_dt=today,
@@ -245,7 +245,7 @@ def test_expiry_last_relative_subevents(event):
)
event.settings.set('payment_term_last', RelativeDateWrapper(
- RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
+ RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
order = _create_order(event, email='dummy@example.org', positions=[cp1, cp2],
now_dt=today,
@@ -286,6 +286,30 @@ def test_expiry_dst(event):
assert (localex.hour, localex.minute) == (23, 59)
+@pytest.mark.django_db
+def test_expiry_per_channel(event):
+ today = now()
+ event.settings.set('payment_term_mode', 'minutes')
+ event.settings.set('payment_term_minutes', 30)
+ event.settings.set('payment_term_mode_baz', 'minutes')
+ event.settings.set('payment_term_minutes_baz', 15)
+ order = _create_order(event, email='dummy@example.org', positions=[],
+ now_dt=today,
+ sales_channel=event.organizer.sales_channels.get(identifier="baz"),
+ payment_requests=[{
+ "id": "test0",
+ "provider": "free",
+ "max_value": None,
+ "min_value": None,
+ "multi_use_supported": False,
+ "info_data": {},
+ "pprov": FreeOrderProvider(event),
+ }],
+ locale='de')[0]
+ assert (order.expires - today).days == 0
+ assert (order.expires - today).seconds == 15 * 60
+
+
@pytest.mark.django_db
def test_expiring(event):
o1 = Order.objects.create(
diff --git a/src/tests/base/test_payment.py b/src/tests/base/test_payment.py
index 9b88e37a7..a2823b915 100644
--- a/src/tests/base/test_payment.py
+++ b/src/tests/base/test_payment.py
@@ -133,7 +133,7 @@ def test_availability_date_relative(event):
event.save()
prov = DummyPaymentProvider(event)
prov.settings.set('_availability_date', RelativeDateWrapper(
- RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
+ RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
utc = datetime.timezone.utc
@@ -150,7 +150,7 @@ def test_availability_start_relative(event):
event.save()
prov = DummyPaymentProvider(event)
prov.settings.set('_availability_start', RelativeDateWrapper(
- RelativeDate(days=2, time=datetime.time(12, 0), base_date_name='date_from', minutes=None)
+ RelativeDate(days=2, time=datetime.time(12, 0), base_date_name='event__date_from', minutes=None)
))
utc = datetime.timezone.utc
@@ -193,12 +193,12 @@ def test_availability_date_cart_relative_subevents(event):
prov = DummyPaymentProvider(event)
prov.settings.set('_availability_date', RelativeDateWrapper(
- RelativeDate(days=3, time=None, base_date_name='date_from', minutes=None)
+ RelativeDate(days=3, time=None, base_date_name='event__date_from', minutes=None)
))
assert prov._is_available_by_time(cart_id="123")
prov.settings.set('_availability_date', RelativeDateWrapper(
- RelativeDate(days=4, time=None, base_date_name='date_from', minutes=None)
+ RelativeDate(days=4, time=None, base_date_name='event__date_from', minutes=None)
))
assert not prov._is_available_by_time(cart_id="123")
@@ -233,11 +233,11 @@ def test_availability_date_order_relative_subevents(event):
prov = DummyPaymentProvider(event)
prov.settings.set('_availability_date', RelativeDateWrapper(
- RelativeDate(days=3, time=None, base_date_name='date_from', minutes=None)
+ RelativeDate(days=3, time=None, base_date_name='event__date_from', minutes=None)
))
assert prov._is_available_by_time(order=order)
prov.settings.set('_availability_date', RelativeDateWrapper(
- RelativeDate(days=4, time=None, base_date_name='date_from', minutes=None)
+ RelativeDate(days=4, time=None, base_date_name='event__date_from', minutes=None)
))
assert not prov._is_available_by_time(order=order)
diff --git a/src/tests/base/test_reldate.py b/src/tests/base/test_reldate.py
index d5f0db9ec..92cefb1a5 100644
--- a/src/tests/base/test_reldate.py
+++ b/src/tests/base/test_reldate.py
@@ -19,13 +19,13 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# .
#
-from datetime import datetime, time
+from datetime import datetime, time, timedelta
from zoneinfo import ZoneInfo
import pytest
from django_scopes import scope
-from pretix.base.models import Event, Organizer
+from pretix.base.models import Event, Order, Organizer
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
TOKYO = ZoneInfo('Asia/Tokyo')
@@ -40,7 +40,6 @@ def event():
date_from=datetime(2017, 12, 27, 5, 0, 0, tzinfo=TOKYO),
presale_start=datetime(2017, 12, 1, 5, 0, 0, tzinfo=TOKYO),
plugins='pretix.plugins.banktransfer'
-
)
event.settings.timezone = "Asia/Tokyo"
return event
@@ -56,57 +55,57 @@ def test_absolute_date(event):
@pytest.mark.django_db
def test_relative_date_without_time(event):
- rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='date_from', minutes=None))
+ rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='event__date_from', minutes=None))
assert rdw.datetime(event).astimezone(TOKYO) == datetime(2017, 12, 26, 5, 0, 0, tzinfo=TOKYO)
- assert rdw.to_string() == 'RELDATE/1/-/date_from/'
- rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='date_from', minutes=None, is_after=True))
+ assert rdw.to_string() == 'RELDATE/1/-/event__date_from/'
+ rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='event__date_from', minutes=None, is_after=True))
assert rdw.datetime(event).astimezone(TOKYO) == datetime(2017, 12, 28, 5, 0, 0, tzinfo=TOKYO)
- assert rdw.to_string() == 'RELDATE/1/-/date_from/after'
+ assert rdw.to_string() == 'RELDATE/1/-/event__date_from/after'
@pytest.mark.django_db
def test_relative_date_other_base_point(event):
with scope(organizer=event.organizer):
- rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='presale_start', minutes=None))
+ rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='event__presale_start', minutes=None))
assert rdw.datetime(event) == datetime(2017, 11, 30, 5, 0, 0, tzinfo=TOKYO)
- assert rdw.to_string() == 'RELDATE/1/-/presale_start/'
+ assert rdw.to_string() == 'RELDATE/1/-/event__presale_start/'
# presale_end is unset, defaults to date_from
- rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='presale_end', minutes=None))
+ rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='event__presale_end', minutes=None))
assert rdw.datetime(event) == datetime(2017, 12, 26, 5, 0, 0, tzinfo=TOKYO)
- assert rdw.to_string() == 'RELDATE/1/-/presale_end/'
+ assert rdw.to_string() == 'RELDATE/1/-/event__presale_end/'
# subevent base
se = event.subevents.create(name="SE1", date_from=datetime(2017, 11, 27, 5, 0, 0, tzinfo=TOKYO))
- rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='date_from', minutes=None))
+ rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='event__date_from', minutes=None))
assert rdw.datetime(se) == datetime(2017, 11, 26, 5, 0, 0, tzinfo=TOKYO)
# presale_start is unset on subevent, default to event
- rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='presale_start', minutes=None))
+ rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='event__presale_start', minutes=None))
assert rdw.datetime(se) == datetime(2017, 11, 30, 5, 0, 0, tzinfo=TOKYO)
# presale_end is unset on all, default to date_from of subevent
- rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='presale_end', minutes=None))
+ rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='event__presale_end', minutes=None))
assert rdw.datetime(se) == datetime(2017, 11, 26, 5, 0, 0, tzinfo=TOKYO)
@pytest.mark.django_db
def test_relative_date_in_minutes(event):
- rdw = RelativeDateWrapper(RelativeDate(days=0, time=None, base_date_name='date_from', minutes=60))
- assert rdw.to_string() == 'RELDATE/minutes/60/date_from/'
+ rdw = RelativeDateWrapper(RelativeDate(days=0, time=None, base_date_name='event__date_from', minutes=60))
+ assert rdw.to_string() == 'RELDATE/minutes/60/event__date_from/'
assert rdw.datetime(event) == datetime(2017, 12, 27, 4, 0, 0, tzinfo=TOKYO)
- rdw = RelativeDateWrapper(RelativeDate(days=0, time=None, base_date_name='date_from', minutes=60, is_after=True))
- assert rdw.to_string() == 'RELDATE/minutes/60/date_from/after'
+ rdw = RelativeDateWrapper(RelativeDate(days=0, time=None, base_date_name='event__date_from', minutes=60, is_after=True))
+ assert rdw.to_string() == 'RELDATE/minutes/60/event__date_from/after'
assert rdw.datetime(event) == datetime(2017, 12, 27, 6, 0, 0, tzinfo=TOKYO)
@pytest.mark.django_db
def test_relative_date_with_time(event):
- rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(8, 5, 13), base_date_name='date_from', minutes=None))
- assert rdw.to_string() == 'RELDATE/1/08:05:13/date_from/'
+ rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(8, 5, 13), base_date_name='event__date_from', minutes=None))
+ assert rdw.to_string() == 'RELDATE/1/08:05:13/event__date_from/'
assert rdw.datetime(event) == datetime(2017, 12, 26, 8, 5, 13, tzinfo=TOKYO)
- rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(8, 5, 13), base_date_name='date_from', minutes=None, is_after=True))
- assert rdw.to_string() == 'RELDATE/1/08:05:13/date_from/after'
+ rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(8, 5, 13), base_date_name='event__date_from', minutes=None, is_after=True))
+ assert rdw.to_string() == 'RELDATE/1/08:05:13/event__date_from/after'
assert rdw.datetime(event) == datetime(2017, 12, 28, 8, 5, 13, tzinfo=TOKYO)
@@ -115,35 +114,94 @@ def test_relative_date_with_time_around_dst(event):
event.settings.timezone = "Europe/Berlin"
event.date_from = datetime(2020, 3, 29, 18, 0, 0, tzinfo=BERLIN)
- rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(18, 0, 0), base_date_name='date_from', minutes=None))
- assert rdw.to_string() == 'RELDATE/1/18:00:00/date_from/'
+ rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(18, 0, 0), base_date_name='event__date_from', minutes=None))
+ assert rdw.to_string() == 'RELDATE/1/18:00:00/event__date_from/'
assert rdw.datetime(event) == datetime(2020, 3, 28, 18, 0, 0, tzinfo=BERLIN)
- rdw = RelativeDateWrapper(RelativeDate(days=0, time=time(2, 30, 0), base_date_name='date_from', minutes=None))
- assert rdw.to_string() == 'RELDATE/0/02:30:00/date_from/'
+ rdw = RelativeDateWrapper(RelativeDate(days=0, time=time(2, 30, 0), base_date_name='event__date_from', minutes=None))
+ assert rdw.to_string() == 'RELDATE/0/02:30:00/event__date_from/'
assert rdw.datetime(event) == datetime(2020, 3, 29, 2, 30, 0, tzinfo=BERLIN)
event.date_from = datetime(2020, 10, 25, 18, 0, 0, tzinfo=BERLIN)
- rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(18, 0, 0), base_date_name='date_from', minutes=None))
- assert rdw.to_string() == 'RELDATE/1/18:00:00/date_from/'
+ rdw = RelativeDateWrapper(RelativeDate(days=1, time=time(18, 0, 0), base_date_name='event__date_from', minutes=None))
+ assert rdw.to_string() == 'RELDATE/1/18:00:00/event__date_from/'
assert rdw.datetime(event) == datetime(2020, 10, 24, 18, 0, 0, tzinfo=BERLIN)
- rdw = RelativeDateWrapper(RelativeDate(days=0, time=time(2, 30, 0), base_date_name='date_from', minutes=None))
- assert rdw.to_string() == 'RELDATE/0/02:30:00/date_from/'
+ rdw = RelativeDateWrapper(RelativeDate(days=0, time=time(2, 30, 0), base_date_name='event__date_from', minutes=None))
+ assert rdw.to_string() == 'RELDATE/0/02:30:00/event__date_from/'
assert rdw.datetime(event) == datetime(2020, 10, 25, 2, 30, 0, tzinfo=BERLIN)
-def test_unserialize():
+def test_unserialize_backwards_compatibility():
+ d = datetime(2017, 12, 25, 10, 0, 0, tzinfo=TOKYO)
+ rdw = RelativeDateWrapper.from_string(d.isoformat())
+ assert rdw.data == d
+
+ # keeping the test for the old from_string_format to ensure that we don't break anything
+ rdw = RelativeDateWrapper.from_string('RELDATE/1/-/date_from/')
+ assert rdw.data == RelativeDate(days=1, time=None, base_date_name='date_from', minutes=None)
+
+ # keeping the test for the old from_string_format to ensure that we don't break anything
+ rdw = RelativeDateWrapper.from_string('RELDATE/1/18:05:13/date_from/')
+ assert rdw.data == RelativeDate(days=1, time=time(18, 5, 13), base_date_name='date_from', minutes=None)
+
+ # keeping the test for the old from_string_format to ensure that we don't break anything
+ rdw = RelativeDateWrapper.from_string('RELDATE/minutes/60/date_from/')
+ assert rdw.data == RelativeDate(days=0, time=None, base_date_name='date_from', minutes=60)
+
+
+def test_backwards_compatibility():
+ # the data model of RelativeDate had to be extended to support other models as relation target
+ # previously only:
+ # - date_from
+ # - date_to
+ # - date_admission
+ # - presale_start
+ # - presale_end
+ # where valid values for the fourth slot (base_date_names) of the serialized form.
+ # the relationship in this case always pointed at event
+ # so any preexisting base_date_names without __ should continue to work and upgrade to event__{old_base_date_name}
d = datetime(2017, 12, 25, 10, 0, 0, tzinfo=TOKYO)
rdw = RelativeDateWrapper.from_string(d.isoformat())
assert rdw.data == d
rdw = RelativeDateWrapper.from_string('RELDATE/1/-/date_from/')
- assert rdw.data == RelativeDate(days=1, time=None, base_date_name='date_from', minutes=None)
+ assert rdw.to_string() == 'RELDATE/1/-/event__date_from/'
- rdw = RelativeDateWrapper.from_string('RELDATE/1/18:05:13/date_from/')
- assert rdw.data == RelativeDate(days=1, time=time(18, 5, 13), base_date_name='date_from', minutes=None)
+ rdw = RelativeDateWrapper.from_string('RELDATE/1/-/date_to/')
+ assert rdw.to_string() == 'RELDATE/1/-/event__date_to/'
- rdw = RelativeDateWrapper.from_string('RELDATE/minutes/60/date_from/')
- assert rdw.data == RelativeDate(days=0, time=None, base_date_name='date_from', minutes=60)
+ rdw = RelativeDateWrapper.from_string('RELDATE/1/-/date_admission/')
+ assert rdw.to_string() == 'RELDATE/1/-/event__date_admission/'
+
+ rdw = RelativeDateWrapper.from_string('RELDATE/1/-/presale_start/')
+ assert rdw.to_string() == 'RELDATE/1/-/event__presale_start/'
+
+ rdw = RelativeDateWrapper.from_string('RELDATE/1/-/presale_end/')
+ assert rdw.to_string() == 'RELDATE/1/-/event__presale_end/'
+
+ # new order base_date_names should not work without __
+ with pytest.raises(TypeError):
+ RelativeDateWrapper.from_string('RELDATE/1/-/datetime/')
+ with pytest.raises(TypeError):
+ RelativeDateWrapper.from_string('RELDATE/1/-/expires/')
+
+
+@pytest.mark.django_db
+def test_relative_to_order(event):
+ with scope(organizer=event.organizer):
+ order_moment = datetime(2020, 3, 29, 18, 0, 0, tzinfo=TOKYO)
+
+ order = Order.objects.create(
+ code='FOO', event=event, email='dummy@dummy.test',
+ status=Order.STATUS_PENDING, secret="k24fiuwvu8kxz3y1",
+ datetime=order_moment,
+ expires=order_moment + timedelta(days=10),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
+ total=23, locale='en'
+ )
+
+ rdw = RelativeDateWrapper(RelativeDate(days=1, time=None, base_date_name='order__datetime', minutes=None, is_after=True))
+ assert rdw.datetime(order).astimezone(TOKYO) == datetime(2020, 3, 30, 18, 0, 0, tzinfo=TOKYO)
+ assert rdw.to_string() == 'RELDATE/1/-/order__datetime/after'
diff --git a/src/tests/base/test_shredders.py b/src/tests/base/test_shredders.py
index d2f0c59c8..b5633d7e7 100644
--- a/src/tests/base/test_shredders.py
+++ b/src/tests/base/test_shredders.py
@@ -342,8 +342,11 @@ def test_invoice_shredder(event, order):
s.shred_data()
inv.refresh_from_db()
- assert "Acme" not in inv.invoice_to
- assert "icket" not in inv.lines.first().description
+ assert inv.invoice_to == "█"
+ assert inv.invoice_to_company == "█"
+ l = inv.lines.first()
+ assert l.description == "█"
+ assert l.attendee_name == "█"
assert not inv.file
assert not os.path.exists(fname)
diff --git a/src/tests/base/test_templatetag.py b/src/tests/base/test_templatetag.py
index d2041e5d3..c7e71bb86 100644
--- a/src/tests/base/test_templatetag.py
+++ b/src/tests/base/test_templatetag.py
@@ -26,7 +26,7 @@ from django.template import Context, Template
from django.test import RequestFactory
from django.utils import translation
-from pretix.base.templatetags.money import money_filter
+from pretix.base.templatetags.money import money_filter, tax_rate_format
TEMPLATE_REPLACE_PAGE = Template(
"{% load urlreplace %}{% url_replace request 'page' 3 %}"
@@ -60,7 +60,9 @@ def test_urlreplace_replace_parameter():
"locale,amount,currency,expected",
[
("en", None, "USD", "$0.00"),
+ ("en", "", "USD", ""),
("en", 1000000, "USD", "$1,000,000.00"),
+ ("en", 2.23, "USD", "$2.23"),
("en", Decimal("1000.00"), "USD", "$1,000.00"),
("de", Decimal("1.23"), "EUR", "1,23" + NBSP + "€"),
("de", Decimal("1000.00"), "EUR", "1.000,00" + NBSP + "€"),
@@ -70,11 +72,14 @@ def test_urlreplace_replace_parameter():
# unknown currency
("de", Decimal("1234.56"), "FOO", "1.234,56" + NBSP + "FOO"),
- ("de", Decimal("1234.567"), "FOO", "1.234,57" + NBSP + "FOO"),
+ ("de", Decimal("1234.567"), "FOO", "1.234,567" + NBSP + "FOO"),
- # rounding errors
- ("de", Decimal("1.234"), "EUR", "1,23" + NBSP + "€"),
- ("de", Decimal("1023.1"), "JPY", "JPY 1.023,10"),
+ # deal with precision that is higher than the currency
+ ("de", Decimal("1.234"), "EUR", "1,234" + NBSP + "€"),
+ ("de", 1.234, "EUR", "1,234" + NBSP + "€"),
+ ("de", Decimal("1.2340"), "EUR", "1,234" + NBSP + "€"),
+ ("de", Decimal("1.2300"), "EUR", "1,23" + NBSP + "€"),
+ ("de", Decimal("1023.1"), "JPY", "1.023,10" + NBSP + "¥"),
]
)
def test_money_filter(locale, amount, currency, expected):
@@ -98,9 +103,31 @@ def test_money_filter(locale, amount, currency, expected):
[
("de", Decimal("1000.00"), "EUR", "1.000,00"),
("en", Decimal("1000.00"), "EUR", "1,000.00"),
- ("de", Decimal("1023.1"), "JPY", "1.023,10"),
+ ("de", Decimal("1023.1"), "JPY", "1.023,1"),
]
)
def test_money_filter_hidecurrency(locale, amount, currency, expected):
translation.activate(locale)
assert money_filter(amount, currency, hide_currency=True) == expected
+
+
+@pytest.mark.parametrize(
+ "locale,rate,expected",
+ [
+ ("de", Decimal("2.00"), "2"),
+ ("de", Decimal("2.50"), "2,5"),
+ ("de", Decimal("2.2340"), "2,234"),
+ ("en", Decimal("2.00"), "2"),
+ ("en", Decimal("2.50"), "2.5"),
+ ("en", Decimal("4.3e7"), "43000000"),
+ ("en", Decimal("2.2340"), "2.234"),
+ ("en", "2.23", "2.23"),
+ ("en", 2.23, "2.23"),
+ ("en", 2, "2"),
+ ("en", "", ""),
+ ("en", None, "0"),
+ ]
+)
+def test_tax_rate_format(locale, rate, expected):
+ translation.activate(locale)
+ assert tax_rate_format(rate) == expected
diff --git a/src/tests/base/test_waitinglist.py b/src/tests/base/test_waitinglist.py
index df90eaa87..ffa155d9e 100644
--- a/src/tests/base/test_waitinglist.py
+++ b/src/tests/base/test_waitinglist.py
@@ -235,7 +235,7 @@ class WaitingListTestCase(TestCase):
self.event.settings.set('waiting_list_enabled', True)
self.event.settings.set('waiting_list_auto', True)
self.event.settings.waiting_list_auto_disable = RelativeDateWrapper(
- RelativeDate(days=0, time=None, base_date_name='date_from', minutes=20, is_after=False)
+ RelativeDate(days=0, time=None, base_date_name='event__date_from', minutes=20, is_after=False)
)
self.event.save()
with scope(organizer=self.o):
diff --git a/src/tests/conftest.py b/src/tests/conftest.py
index da5628856..2406c23e3 100644
--- a/src/tests/conftest.py
+++ b/src/tests/conftest.py
@@ -21,6 +21,8 @@
#
import inspect
import os
+import re
+from pathlib import Path
import pytest
from django.core.cache import cache
@@ -28,7 +30,6 @@ from django.test import override_settings
from django.utils import translation
from django_scopes import scopes_disabled
from fakeredis import FakeRedisConnection
-from hierarkey.proxy import dirty_cache_keys
from xdist.dsession import DSession
from pretix.testutils.mock import get_redis_connection
@@ -85,6 +86,7 @@ def reset_locale():
@pytest.fixture(autouse=True)
def reset_hierarkey_cache_state():
+ from hierarkey.proxy import dirty_cache_keys
dirty_cache_keys.set(set())
@@ -143,3 +145,23 @@ def set_lock_namespaces(request):
@pytest.fixture
def class_monkeypatch(request, monkeypatch):
request.cls.monkeypatch = monkeypatch
+
+
+def pytest_addoption(parser):
+ parser.addoption(
+ '--pdf-dir', action='store', help="output dir for files generated by pdf tests", type=Path
+ )
+ parser.addoption(
+ '--pdf-tmp-keep', action='store_true', help="keep the temporary directory created for pdf tests"
+ )
+
+
+@pytest.fixture
+def pdf_dir(request, tmp_path_factory):
+ dirname = re.sub('[^a-zA-Z0-9]+', '_', request.node.name).strip("_")
+ if dir := request.config.getoption('--pdf-dir'):
+ pdf_dir: Path = (dir / dirname).absolute()
+ pdf_dir.mkdir(exist_ok=True, parents=True)
+ yield pdf_dir
+ else:
+ yield tmp_path_factory.mktemp(dirname)
diff --git a/src/tests/control/test_events.py b/src/tests/control/test_events.py
index efd8a64d8..e363c4889 100644
--- a/src/tests/control/test_events.py
+++ b/src/tests/control/test_events.py
@@ -481,7 +481,7 @@ class EventsTest(SoupTest):
'payment_term_last_0': 'absolute',
'payment_term_last_1': (self.event1.presale_end - datetime.timedelta(1)).strftime('%Y-%m-%d'),
'payment_term_last_2': '0',
- 'payment_term_last_3': 'date_from',
+ 'payment_term_last_3': 'event__date_from',
'tax_rule_payment': 'default',
})
assert doc.select('.alert-danger')
@@ -496,7 +496,7 @@ class EventsTest(SoupTest):
'payment_term_last_0': 'relative',
'payment_term_last_1': '',
'payment_term_last_2': '10',
- 'payment_term_last_3': 'date_from',
+ 'payment_term_last_3': 'event__date_from',
'tax_rule_payment': 'default',
})
assert doc.select('.alert-danger')
diff --git a/src/tests/control/test_modelimport.py b/src/tests/control/test_modelimport.py
index 87726bc92..e41c6323e 100644
--- a/src/tests/control/test_modelimport.py
+++ b/src/tests/control/test_modelimport.py
@@ -32,7 +32,7 @@ def env():
o = Organizer.objects.create(name='Dummy', slug='dummy')
event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
- date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
+ date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal2'
)
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
t = Team.objects.create(organizer=event.organizer, all_event_permissions=True)
diff --git a/src/tests/control/test_orders.py b/src/tests/control/test_orders.py
index f773c5be3..92e20a85e 100644
--- a/src/tests/control/test_orders.py
+++ b/src/tests/control/test_orders.py
@@ -2438,6 +2438,47 @@ def test_refund_paid_order_offsetting_to_wrong_currency(client, env):
assert b'different currency' in r.content
+@pytest.mark.django_db
+def test_refund_paid_order_offsetting_to_wrong_permissions(client, env):
+ with scopes_disabled():
+ p = env[2].payments.last()
+ p.confirm()
+ t = env[0].organizer.teams.get()
+ t.all_events = False
+ t.save()
+ t.limit_events.set([env[0]])
+ client.login(email='dummy@dummy.dummy', password='dummy')
+ event2 = Event.objects.create(
+ organizer=env[0].organizer, name='Dummy', slug='dummy2',
+ date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.stripe,tests.testdummy',
+ currency='EUR',
+ )
+ ticket2 = Item.objects.create(event=event2, name='Early-bird ticket',
+ category=None, default_price=23,
+ admission=True, personalized=True)
+ o = Order.objects.create(
+ code='BAZ', event=event2, email='dummy@dummy.test',
+ status=Order.STATUS_PENDING,
+ datetime=now(), expires=now() + timedelta(days=10),
+ sales_channel=event2.organizer.sales_channels.get(identifier="web"),
+ total=5, locale='en'
+ )
+ o.positions.create(price=5, item=ticket2)
+
+ r = client.post('/control/event/dummy/dummy/orders/ABC32/refund', {
+ 'start-partial_amount': '5.00',
+ 'start-mode': 'partial',
+ 'start-action': 'mark_pending',
+ 'refund-offsetting': '5.00',
+ 'order-offsetting': 'BAZ',
+ 'manual_state': 'pending',
+ 'last_known_refund_id': 0,
+ 'perform': 'on'
+ }, follow=True)
+ assert b'alert-danger' in r.content
+ assert b'do not have access' in r.content
+
+
@pytest.mark.django_db
def test_refund_paid_order_offsetting(client, env):
with scopes_disabled():
diff --git a/src/tests/control/test_subevents.py b/src/tests/control/test_subevents.py
index 1089e9891..532b1fa66 100644
--- a/src/tests/control/test_subevents.py
+++ b/src/tests/control/test_subevents.py
@@ -230,19 +230,19 @@ class SubEventsTest(SoupTest):
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
- 'rel_presale_start_3': 'date_from',
+ 'rel_presale_start_3': 'event__date_from',
'rel_presale_start_4': '',
'rel_presale_start_5': '',
- 'rel_presale_start_6': 'date_from',
+ 'rel_presale_start_6': 'event__date_from',
'rel_presale_start_7': 'before',
'rel_presale_start_8': 'before',
'rel_presale_end_1': '',
'rel_presale_end_0': 'relative',
'rel_presale_end_2': '1',
- 'rel_presale_end_3': 'date_from',
+ 'rel_presale_end_3': 'event__date_from',
'rel_presale_end_4': '13:29:31',
'rel_presale_end_5': '',
- 'rel_presale_end_6': 'date_from',
+ 'rel_presale_end_6': 'event__date_from',
'rel_presale_end_7': 'before',
'rel_presale_end_8': 'before',
'quotas-TOTAL_FORMS': '1',
@@ -328,19 +328,19 @@ class SubEventsTest(SoupTest):
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
- 'rel_presale_start_3': 'date_from',
+ 'rel_presale_start_3': 'event__date_from',
'rel_presale_start_4': '',
'rel_presale_start_5': '',
- 'rel_presale_start_6': 'date_from',
+ 'rel_presale_start_6': 'event__date_from',
'rel_presale_start_7': 'before',
'rel_presale_start_8': 'before',
'rel_presale_end_1': '',
'rel_presale_end_0': 'relative',
'rel_presale_end_2': '1',
- 'rel_presale_end_3': 'date_from',
+ 'rel_presale_end_3': 'event__date_from',
'rel_presale_end_4': '13:29:31',
'rel_presale_end_5': '',
- 'rel_presale_end_6': 'date_from',
+ 'rel_presale_end_6': 'event__date_from',
'rel_presale_end_7': 'before',
'rel_presale_end_8': 'before',
'quotas-TOTAL_FORMS': '1',
@@ -403,19 +403,19 @@ class SubEventsTest(SoupTest):
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
- 'rel_presale_start_3': 'date_from',
+ 'rel_presale_start_3': 'event__date_from',
'rel_presale_start_4': '',
'rel_presale_start_5': '',
- 'rel_presale_start_6': 'date_from',
+ 'rel_presale_start_6': 'event__date_from',
'rel_presale_start_7': 'before',
'rel_presale_start_8': 'before',
'rel_presale_end_1': '',
'rel_presale_end_0': 'relative',
'rel_presale_end_2': '1',
- 'rel_presale_end_3': 'date_from',
+ 'rel_presale_end_3': 'event__date_from',
'rel_presale_end_4': '13:29:31',
'rel_presale_end_5': '',
- 'rel_presale_end_6': 'date_from',
+ 'rel_presale_end_6': 'event__date_from',
'rel_presale_end_7': 'before',
'rel_presale_end_8': 'before',
'quotas-TOTAL_FORMS': '1',
@@ -479,19 +479,19 @@ class SubEventsTest(SoupTest):
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
- 'rel_presale_start_3': 'date_from',
+ 'rel_presale_start_3': 'event__date_from',
'rel_presale_start_4': '',
'rel_presale_start_5': '',
- 'rel_presale_start_6': 'date_from',
+ 'rel_presale_start_6': 'event__date_from',
'rel_presale_start_7': 'before',
'rel_presale_start_8': 'before',
'rel_presale_end_1': '',
'rel_presale_end_0': 'relative',
'rel_presale_end_2': '1',
- 'rel_presale_end_3': 'date_from',
+ 'rel_presale_end_3': 'event__date_from',
'rel_presale_end_4': '02:30:00',
'rel_presale_end_5': '',
- 'rel_presale_end_6': 'date_from',
+ 'rel_presale_end_6': 'event__date_from',
'rel_presale_end_7': 'before',
'rel_presale_end_8': 'before',
'quotas-TOTAL_FORMS': '1',
@@ -567,19 +567,19 @@ class SubEventsTest(SoupTest):
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
- 'rel_presale_start_3': 'date_from',
+ 'rel_presale_start_3': 'event__date_from',
'rel_presale_start_4': '',
'rel_presale_start_5': '',
- 'rel_presale_start_6': 'date_from',
+ 'rel_presale_start_6': 'event__date_from',
'rel_presale_start_7': 'before',
'rel_presale_start_8': 'before',
'rel_presale_end_1': '',
'rel_presale_end_0': 'relative',
'rel_presale_end_2': '1',
- 'rel_presale_end_3': 'date_from',
+ 'rel_presale_end_3': 'event__date_from',
'rel_presale_end_4': '13:29:31',
'rel_presale_end_5': '',
- 'rel_presale_end_6': 'date_from',
+ 'rel_presale_end_6': 'event__date_from',
'rel_presale_end_7': 'before',
'rel_presale_end_8': 'before',
'quotas-TOTAL_FORMS': '1',
@@ -639,19 +639,19 @@ class SubEventsTest(SoupTest):
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
- 'rel_presale_start_3': 'date_from',
+ 'rel_presale_start_3': 'event__date_from',
'rel_presale_start_4': '',
'rel_presale_start_5': '',
- 'rel_presale_start_6': 'date_from',
+ 'rel_presale_start_6': 'event__date_from',
'rel_presale_start_7': 'before',
'rel_presale_start_8': 'before',
'rel_presale_end_0': 'unset',
'rel_presale_end_1': '',
'rel_presale_end_2': '1',
- 'rel_presale_end_3': 'date_from',
+ 'rel_presale_end_3': 'event__date_from',
'rel_presale_end_4': '13:29:31',
'rel_presale_end_5': '',
- 'rel_presale_end_6': 'date_from',
+ 'rel_presale_end_6': 'event__date_from',
'rel_presale_end_7': 'before',
'rel_presale_end_8': 'before',
'quotas-TOTAL_FORMS': '1',
@@ -711,19 +711,19 @@ class SubEventsTest(SoupTest):
'rel_presale_start_0': 'unset',
'rel_presale_start_1': '',
'rel_presale_start_2': '1',
- 'rel_presale_start_3': 'date_from',
+ 'rel_presale_start_3': 'event__date_from',
'rel_presale_start_4': '',
'rel_presale_start_5': '',
- 'rel_presale_start_6': 'date_from',
+ 'rel_presale_start_6': 'event__date_from',
'rel_presale_start_7': 'before',
'rel_presale_start_8': 'before',
'rel_presale_end_0': 'unset',
'rel_presale_end_1': '',
'rel_presale_end_2': '1',
- 'rel_presale_end_3': 'date_from',
+ 'rel_presale_end_3': 'event__date_from',
'rel_presale_end_4': '13:29:31',
'rel_presale_end_5': '',
- 'rel_presale_end_6': 'date_from',
+ 'rel_presale_end_6': 'event__date_from',
'rel_presale_end_7': 'before',
'rel_presale_end_8': 'before',
'quotas-TOTAL_FORMS': '1',
diff --git a/src/tests/control/test_user.py b/src/tests/control/test_user.py
index a7a8f86c0..78992f25a 100644
--- a/src/tests/control/test_user.py
+++ b/src/tests/control/test_user.py
@@ -349,25 +349,25 @@ class UserSettings2FATest(SoupTest):
def test_delete_u2f(self):
d = U2FDevice.objects.create(user=self.user, name='Test')
- self.client.get('/control/settings/2fa/u2f/{}/delete'.format(d.pk))
- self.client.post('/control/settings/2fa/u2f/{}/delete'.format(d.pk))
+ self.client.get('/control/settings/2fa/pretixbase.u2fdevice/{}/delete'.format(d.pk))
+ self.client.post('/control/settings/2fa/pretixbase.u2fdevice/{}/delete'.format(d.pk))
assert not U2FDevice.objects.exists()
def test_delete_webauthn(self):
d = WebAuthnDevice.objects.create(user=self.user, name='Test')
- self.client.get('/control/settings/2fa/webauthn/{}/delete'.format(d.pk))
- self.client.post('/control/settings/2fa/webauthn/{}/delete'.format(d.pk))
+ self.client.get('/control/settings/2fa/pretixbase.webauthndevice/{}/delete'.format(d.pk))
+ self.client.post('/control/settings/2fa/pretixbase.webauthndevice/{}/delete'.format(d.pk))
assert not WebAuthnDevice.objects.exists()
def test_delete_totp(self):
d = TOTPDevice.objects.create(user=self.user, name='Test')
- self.client.get('/control/settings/2fa/totp/{}/delete'.format(d.pk))
- self.client.post('/control/settings/2fa/totp/{}/delete'.format(d.pk))
+ self.client.get('/control/settings/2fa/otp_totp.totpdevice/{}/delete'.format(d.pk))
+ self.client.post('/control/settings/2fa/otp_totp.totpdevice/{}/delete'.format(d.pk))
assert not TOTPDevice.objects.exists()
def test_create_webauthn_require_https(self):
r = self.client.post('/control/settings/2fa/add', {
- 'devicetype': 'webauthn',
+ 'devicetype': 'pretixbase.webauthndevice',
'name': 'Foo'
})
assert 'alert-danger' in r.content.decode()
@@ -376,7 +376,7 @@ class UserSettings2FATest(SoupTest):
with mocker_context() as mocker:
mocker.patch('django.http.request.HttpRequest.is_secure')
self.client.post('/control/settings/2fa/add', {
- 'devicetype': 'webauthn',
+ 'devicetype': 'pretixbase.webauthndevice',
'name': 'Foo'
})
d = WebAuthnDevice.objects.first()
@@ -385,7 +385,7 @@ class UserSettings2FATest(SoupTest):
def test_create_totp(self):
self.client.post('/control/settings/2fa/add', {
- 'devicetype': 'totp',
+ 'devicetype': 'otp_totp.totpdevice',
'name': 'Foo'
})
d = TOTPDevice.objects.first()
@@ -393,7 +393,7 @@ class UserSettings2FATest(SoupTest):
def test_confirm_totp(self):
self.client.post('/control/settings/2fa/add', {
- 'devicetype': 'totp',
+ 'devicetype': 'otp_totp.totpdevice',
'name': 'Foo'
}, follow=True)
d = TOTPDevice.objects.first()
@@ -411,7 +411,7 @@ class UserSettings2FATest(SoupTest):
def test_confirm_totp_failed(self):
self.client.post('/control/settings/2fa/add', {
- 'devicetype': 'totp',
+ 'devicetype': 'otp_totp.totpdevice',
'name': 'Foo'
}, follow=True)
d = TOTPDevice.objects.first()
@@ -428,7 +428,7 @@ class UserSettings2FATest(SoupTest):
with mocker_context() as mocker:
mocker.patch('django.http.request.HttpRequest.is_secure')
self.client.post('/control/settings/2fa/add', {
- 'devicetype': 'webauthn',
+ 'devicetype': 'pretixbase.webauthndevice',
'name': 'Foo'
}, follow=True)
d = WebAuthnDevice.objects.first()
@@ -443,7 +443,7 @@ class UserSettings2FATest(SoupTest):
with mocker_context() as mocker:
mocker.patch('django.http.request.HttpRequest.is_secure')
self.client.post('/control/settings/2fa/add', {
- 'devicetype': 'webauthn',
+ 'devicetype': 'pretixbase.webauthndevice',
'name': 'Foo'
}, follow=True)
diff --git a/src/tests/control/test_views.py b/src/tests/control/test_views.py
index f169df702..1560affe1 100644
--- a/src/tests/control/test_views.py
+++ b/src/tests/control/test_views.py
@@ -224,3 +224,28 @@ def test_one_view(logged_in_client, url, expected, event, item, item_category, o
)
response = logged_in_client.get(url)
assert response.status_code == expected
+
+ # Do not reintroduce any CSP nonces into control responses, as discussed in PR #6387
+ if response['Content-Type'] != 'application/json':
+ assert 'script-src' in response['Content-Security-Policy']
+ assert 'nonce-' not in response['Content-Security-Policy']
+
+
+@pytest.mark.parametrize('url', [
+ '/control/login',
+ '/',
+ '/{orga}/{event}/',
+])
+@pytest.mark.django_db
+def test_csp_header_unauthenticated(client, url, event):
+ # Do not reintroduce any CSP nonces into most presale responses, as discussed in PR #6387
+ with scope(organizer=event.organizer):
+ url = url.format(
+ event=event.slug, orga=event.organizer.slug,
+ )
+ event.live = True
+ event.save()
+ response = client.get(url)
+ assert response.status_code == 200
+ assert 'script-src' in response['Content-Security-Policy']
+ assert 'nonce-' not in response['Content-Security-Policy']
diff --git a/src/tests/control/test_vouchers.py b/src/tests/control/test_vouchers.py
index 586448cd1..4769814a5 100644
--- a/src/tests/control/test_vouchers.py
+++ b/src/tests/control/test_vouchers.py
@@ -128,9 +128,9 @@ class VoucherFormTest(SoupTestMixin, TransactionTestCase):
doc = self.client.get('/control/event/%s/%s/vouchers/?download=yes' % (self.orga.slug, self.event.slug))
assert doc.content.decode().strip() == '"Voucher code","Valid until","Product","Reserve quota",' \
'"Bypass quota","Price effect","Value","Tag","Redeemed",' \
- '"Maximum usages","Seat","Comment"' \
+ '"Maximum usages","Seat","Comment","Budget","Budget used"' \
'\r\n"ABCDEFG","","Early-bird ticket","No","No","No effect","","","0",' \
- '"1","",""'
+ '"1","","","",""'
def test_filter_status_valid(self):
with scopes_disabled():
diff --git a/src/tests/e2e/conftest.py b/src/tests/e2e/conftest.py
index 053073da6..f0308e838 100644
--- a/src/tests/e2e/conftest.py
+++ b/src/tests/e2e/conftest.py
@@ -89,11 +89,9 @@ def _widget_assets():
f'Start it with: npm run dev:widget'
)
yield
- elif os.environ.get("PRETIX_WIDGET_VITE"):
+ else:
subprocess.check_call(['npm', 'run', 'build:widget'], cwd=PROJECT_ROOT)
yield
- else:
- yield # Old widget, no build needed
# ============================================================================
@@ -155,10 +153,6 @@ def live_server_url(live_server, settings):
settings.SITE_URL = live_server.url
- # Enable Vite widget if requested via env var
- if os.environ.get("PRETIX_WIDGET_VITE") or os.environ.get("PRETIX_WIDGET_VITE_DEV"):
- settings.PRETIX_WIDGET_VITE = True
-
return live_server.url
diff --git a/src/pretix/plugins/paypal/urls.py b/src/tests/helpers/test_json.py
similarity index 66%
rename from src/pretix/plugins/paypal/urls.py
rename to src/tests/helpers/test_json.py
index 8b69853e1..6027142f6 100644
--- a/src/pretix/plugins/paypal/urls.py
+++ b/src/tests/helpers/test_json.py
@@ -19,21 +19,21 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# .
#
-from django.urls import include, re_path
+import json
-from .views import abort, oauth_disconnect, success
+import pytest
-event_patterns = [
- re_path(r'^paypal/', include([
- re_path(r'^abort/$', abort, name='abort'),
- re_path(r'^return/$', success, name='return'),
- re_path(r'w/(?P[a-zA-Z0-9]{16})/abort/', abort, name='abort'),
- re_path(r'w/(?P[a-zA-Z0-9]{16})/return/', success, name='return'),
- ])),
-]
+def test_allowed_json():
+ assert json.loads('{"float":1.5,"int":161,"arr":[]}') == {"float": 1.5, "int": 161, "arr": []}
-urlpatterns = [
- re_path(r'^control/event/(?P[^/]+)/(?P[^/]+)/paypal/disconnect/',
- oauth_disconnect, name='oauth.disconnect'),
-]
+
+def test_disallowed_json_float_consts():
+ with pytest.raises(KeyError):
+ json.loads("Infinity")
+ with pytest.raises(KeyError):
+ json.loads("-Infinity")
+ with pytest.raises(KeyError):
+ json.loads("NaN")
+ with pytest.raises(KeyError):
+ json.loads("[123, NaN, Infinity, -Infinity]")
diff --git a/src/tests/multidomain/test_middlewares.py b/src/tests/multidomain/test_middlewares.py
index 9fc488d41..76d345e4f 100644
--- a/src/tests/multidomain/test_middlewares.py
+++ b/src/tests/multidomain/test_middlewares.py
@@ -242,6 +242,24 @@ def test_cookie_domain_on_main_domain(env, client):
assert r.client.cookies['pretix_session']['domain'] == ''
+@pytest.fixture
+def csrf_client():
+ from django.test import Client
+ return Client(enforce_csrf_checks=True)
+
+
+@pytest.mark.django_db
+@pytest.mark.parametrize("url", [
+ '/mrmcd/2015/',
+ '/ensure_csrf_cookie/',
+])
+def test_csrf_secure(env, url, csrf_client):
+ r = csrf_client.get(url, secure=True, HTTP_HOST='example.com')
+ assert 'pretix_csrftoken' not in r.client.cookies
+ assert '__Host-pretix_csrftoken' in r.client.cookies
+ assert r.client.cookies['__Host-pretix_csrftoken']['secure']
+
+
@pytest.mark.django_db
@override_settings(USE_X_FORWARDED_HOST=True)
def test_with_forwarded_host(env, client):
diff --git a/src/tests/multidomain/test_plugin_handler.py b/src/tests/multidomain/test_plugin_handler.py
index 8a8057cb1..d38cd9be0 100644
--- a/src/tests/multidomain/test_plugin_handler.py
+++ b/src/tests/multidomain/test_plugin_handler.py
@@ -37,26 +37,26 @@ def event():
@pytest.mark.django_db
def test_require_plugin(event, client):
- event.plugins = 'pretix.plugins.paypal'
+ event.plugins = 'pretix.plugins.paypal2'
event.live = True
event.save()
- r = client.get('/mrmcd/2015/paypal/abort/', follow=False)
+ r = client.get('/mrmcd/2015/paypal2/abort/', follow=False)
assert r.status_code == 302
event.plugins = ''
event.save()
- r = client.get('/mrmcd/2015/paypal/abort/', follow=False)
+ r = client.get('/mrmcd/2015/paypal2/abort/', follow=False)
assert r.status_code == 404
@pytest.mark.django_db
def test_require_live(event, client):
- event.plugins = 'pretix.plugins.paypal'
+ event.plugins = 'pretix.plugins.paypal2'
event.live = True
event.save()
- r = client.get('/mrmcd/2015/paypal/abort/', follow=False)
+ r = client.get('/mrmcd/2015/paypal2/abort/', follow=False)
assert r.status_code == 302
event.live = False
event.save()
- r = client.get('/mrmcd/2015/paypal/abort/', follow=False)
+ r = client.get('/mrmcd/2015/paypal2/abort/', follow=False)
assert r.status_code == 403
diff --git a/src/tests/plugins/badges/assets/bg-cropbox/bg.pdf b/src/tests/plugins/badges/assets/bg-cropbox/bg.pdf
new file mode 100644
index 000000000..a81d104ae
--- /dev/null
+++ b/src/tests/plugins/badges/assets/bg-cropbox/bg.pdf
@@ -0,0 +1,99 @@
+%PDF-1.3
+%âãÏÓ
+1 0 obj
+<<
+/PageMode /UseNone
+/Pages 2 0 R
+/Type /Catalog
+>>
+endobj
+2 0 obj
+<<
+/Count 1
+/Kids [ 3 0 R ]
+/Type /Pages
+>>
+endobj
+3 0 obj
+<<
+/Contents 4 0 R
+/MediaBox [ 0 0 595.2756 841.8898 ]
+/Parent 2 0 R
+/Resources <<
+/Font 5 0 R
+/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
+>>
+/Rotate 0
+/Trans <<
+>>
+/Type /Page
+/CropBox [ 10 10 307.84 220 ]
+>>
+endobj
+4 0 obj
+<<
+/Length 117
+>>
+stream
+q
+1 0.0 0.0 1 10 10 cm
+1 0 0 1 0 0 cm
+BT
+/F1 12 Tf
+14.4 TL
+ET
+1 0 0 rg
+n
+56.69291 56.69291 141.7323 28.34646 re
+B*
+Q
+
+endstream
+endobj
+5 0 obj
+<<
+/F1 6 0 R
+>>
+endobj
+6 0 obj
+<<
+/BaseFont /Helvetica
+/Encoding /WinAnsiEncoding
+/Name /F1
+/Subtype /Type1
+/Type /Font
+>>
+endobj
+7 0 obj
+<<
+/Author (anonymous)
+/CreationDate (D\07220260917173038\05302\04700\047)
+/Creator (anonymous)
+/Keywords ()
+/ModDate (D\07220260917173038\05302\04700\047)
+/Producer (ReportLab PDF Library \055 \050opensource\051)
+/Subject (unspecified)
+/Title (untitled)
+/Trapped /False
+>>
+endobj
+xref
+0 8
+0000000000 65535 f
+0000000015 00000 n
+0000000083 00000 n
+0000000142 00000 n
+0000000371 00000 n
+0000000539 00000 n
+0000000570 00000 n
+0000000677 00000 n
+trailer
+<<
+/Size 8
+/Root 1 0 R
+/Info 7 0 R
+/ID [ ]
+>>
+startxref
+966
+%%EOF
diff --git a/src/tests/plugins/badges/assets/bg-cropbox/expected.pdf b/src/tests/plugins/badges/assets/bg-cropbox/expected.pdf
new file mode 100644
index 000000000..e0f7a11e4
--- /dev/null
+++ b/src/tests/plugins/badges/assets/bg-cropbox/expected.pdf
@@ -0,0 +1,213 @@
+%PDF-1.3
+%âãÏÓ
+1 0 obj
+<<
+/Producer (pypdf)
+>>
+endobj
+2 0 obj
+<<
+/Type /Pages
+/Count 2
+/Kids [ 4 0 R 11 0 R ]
+>>
+endobj
+3 0 obj
+<<
+/Type /Catalog
+/Pages 2 0 R
+>>
+endobj
+4 0 obj
+<<
+/Contents 5 0 R
+/MediaBox [ 0 0 297.84 210 ]
+/Resources <<
+/Font 6 0 R
+/ProcSet [ /ImageB /ImageC /ImageI /PDF /Text ]
+>>
+/Rotate 0
+/Trans <<
+>>
+/Type /Page
+/Parent 2 0 R
+>>
+endobj
+5 0 obj
+[ 9 0 R 10 0 R ]
+endobj
+6 0 obj
+<<
+/F1 7 0 R
+/F1-0 8 0 R
+>>
+endobj
+7 0 obj
+<<
+/BaseFont /Helvetica
+/Encoding /WinAnsiEncoding
+/Name /F1
+/Subtype /Type1
+/Type /Font
+>>
+endobj
+8 0 obj
+<<
+/BaseFont /Helvetica
+/Encoding /WinAnsiEncoding
+/Name /F1
+/Subtype /Type1
+/Type /Font
+>>
+endobj
+9 0 obj
+<<
+/Length 170
+>>
+stream
+q
+1 0.0 0.0 1 -10 -10 cm
+10 10 297.84 210 re
+W
+n
+q
+1 0.0 0.0 1 10 10 cm
+1 0 0 1 0 0 cm
+BT
+/F1-0 12 Tf
+14.4 TL
+ET
+1 0 0 rg
+n
+56.69291 56.69291 141.7323 28.34646 re
+B*
+Q
+Q
+
+endstream
+endobj
+10 0 obj
+<<
+/Length 106
+>>
+stream
+q
+1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
+q
+1 0 0 1 37.10551 140.9669 cm
+q
+1 0 0 1 0 5.669531 cm
+Q
+Q
+
+
+Q
+
+endstream
+endobj
+11 0 obj
+<<
+/Contents 12 0 R
+/MediaBox [ 0 0 297.84 210 ]
+/Resources <<
+/Font 13 0 R
+/ProcSet [ /ImageB /ImageC /ImageI /PDF /Text ]
+>>
+/Rotate 0
+/Trans <<
+>>
+/Type /Page
+/Parent 2 0 R
+>>
+endobj
+12 0 obj
+[ 15 0 R 16 0 R ]
+endobj
+13 0 obj
+<<
+/F1 14 0 R
+/F1-0 8 0 R
+>>
+endobj
+14 0 obj
+<<
+/BaseFont /Helvetica
+/Encoding /WinAnsiEncoding
+/Name /F1
+/Subtype /Type1
+/Type /Font
+>>
+endobj
+15 0 obj
+<<
+/Length 170
+>>
+stream
+q
+1 0.0 0.0 1 -10 -10 cm
+10 10 297.84 210 re
+W
+n
+q
+1 0.0 0.0 1 10 10 cm
+1 0 0 1 0 0 cm
+BT
+/F1-0 12 Tf
+14.4 TL
+ET
+1 0 0 rg
+n
+56.69291 56.69291 141.7323 28.34646 re
+B*
+Q
+Q
+
+endstream
+endobj
+16 0 obj
+<<
+/Length 106
+>>
+stream
+q
+1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
+q
+1 0 0 1 37.10551 140.9669 cm
+q
+1 0 0 1 0 5.669531 cm
+Q
+Q
+
+
+Q
+
+endstream
+endobj
+xref
+0 17
+0000000000 65535 f
+0000000015 00000 n
+0000000054 00000 n
+0000000120 00000 n
+0000000169 00000 n
+0000000361 00000 n
+0000000393 00000 n
+0000000436 00000 n
+0000000543 00000 n
+0000000650 00000 n
+0000000871 00000 n
+0000001029 00000 n
+0000001224 00000 n
+0000001258 00000 n
+0000001303 00000 n
+0000001411 00000 n
+0000001633 00000 n
+trailer
+<<
+/Size 17
+/Root 3 0 R
+/Info 1 0 R
+>>
+startxref
+1791
+%%EOF
diff --git a/src/tests/plugins/badges/assets/bg-mediabox-offset/bg.pdf b/src/tests/plugins/badges/assets/bg-mediabox-offset/bg.pdf
new file mode 100644
index 000000000..85ac42c23
--- /dev/null
+++ b/src/tests/plugins/badges/assets/bg-mediabox-offset/bg.pdf
@@ -0,0 +1,98 @@
+%PDF-1.3
+%âãÏÓ
+1 0 obj
+<<
+/PageMode /UseNone
+/Pages 2 0 R
+/Type /Catalog
+>>
+endobj
+2 0 obj
+<<
+/Count 1
+/Kids [ 3 0 R ]
+/Type /Pages
+>>
+endobj
+3 0 obj
+<<
+/Contents 4 0 R
+/MediaBox [ 10 10 307.84 220 ]
+/Parent 2 0 R
+/Resources <<
+/Font 5 0 R
+/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
+>>
+/Rotate 0
+/Trans <<
+>>
+/Type /Page
+>>
+endobj
+4 0 obj
+<<
+/Length 117
+>>
+stream
+q
+1 0.0 0.0 1 10 10 cm
+1 0 0 1 0 0 cm
+BT
+/F1 12 Tf
+14.4 TL
+ET
+1 0 0 rg
+n
+56.69291 56.69291 141.7323 28.34646 re
+B*
+Q
+
+endstream
+endobj
+5 0 obj
+<<
+/F1 6 0 R
+>>
+endobj
+6 0 obj
+<<
+/BaseFont /Helvetica
+/Encoding /WinAnsiEncoding
+/Name /F1
+/Subtype /Type1
+/Type /Font
+>>
+endobj
+7 0 obj
+<<
+/Author (anonymous)
+/CreationDate (D\07220260917173038\05302\04700\047)
+/Creator (anonymous)
+/Keywords ()
+/ModDate (D\07220260917173038\05302\04700\047)
+/Producer (ReportLab PDF Library \055 \050opensource\051)
+/Subject (unspecified)
+/Title (untitled)
+/Trapped /False
+>>
+endobj
+xref
+0 8
+0000000000 65535 f
+0000000015 00000 n
+0000000083 00000 n
+0000000142 00000 n
+0000000336 00000 n
+0000000504 00000 n
+0000000535 00000 n
+0000000642 00000 n
+trailer
+<<
+/Size 8
+/Root 1 0 R
+/Info 7 0 R
+/ID [ ]
+>>
+startxref
+931
+%%EOF
diff --git a/src/tests/plugins/badges/assets/bg-mediabox-offset/expected.pdf b/src/tests/plugins/badges/assets/bg-mediabox-offset/expected.pdf
new file mode 100644
index 000000000..e0f7a11e4
--- /dev/null
+++ b/src/tests/plugins/badges/assets/bg-mediabox-offset/expected.pdf
@@ -0,0 +1,213 @@
+%PDF-1.3
+%âãÏÓ
+1 0 obj
+<<
+/Producer (pypdf)
+>>
+endobj
+2 0 obj
+<<
+/Type /Pages
+/Count 2
+/Kids [ 4 0 R 11 0 R ]
+>>
+endobj
+3 0 obj
+<<
+/Type /Catalog
+/Pages 2 0 R
+>>
+endobj
+4 0 obj
+<<
+/Contents 5 0 R
+/MediaBox [ 0 0 297.84 210 ]
+/Resources <<
+/Font 6 0 R
+/ProcSet [ /ImageB /ImageC /ImageI /PDF /Text ]
+>>
+/Rotate 0
+/Trans <<
+>>
+/Type /Page
+/Parent 2 0 R
+>>
+endobj
+5 0 obj
+[ 9 0 R 10 0 R ]
+endobj
+6 0 obj
+<<
+/F1 7 0 R
+/F1-0 8 0 R
+>>
+endobj
+7 0 obj
+<<
+/BaseFont /Helvetica
+/Encoding /WinAnsiEncoding
+/Name /F1
+/Subtype /Type1
+/Type /Font
+>>
+endobj
+8 0 obj
+<<
+/BaseFont /Helvetica
+/Encoding /WinAnsiEncoding
+/Name /F1
+/Subtype /Type1
+/Type /Font
+>>
+endobj
+9 0 obj
+<<
+/Length 170
+>>
+stream
+q
+1 0.0 0.0 1 -10 -10 cm
+10 10 297.84 210 re
+W
+n
+q
+1 0.0 0.0 1 10 10 cm
+1 0 0 1 0 0 cm
+BT
+/F1-0 12 Tf
+14.4 TL
+ET
+1 0 0 rg
+n
+56.69291 56.69291 141.7323 28.34646 re
+B*
+Q
+Q
+
+endstream
+endobj
+10 0 obj
+<<
+/Length 106
+>>
+stream
+q
+1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
+q
+1 0 0 1 37.10551 140.9669 cm
+q
+1 0 0 1 0 5.669531 cm
+Q
+Q
+
+
+Q
+
+endstream
+endobj
+11 0 obj
+<<
+/Contents 12 0 R
+/MediaBox [ 0 0 297.84 210 ]
+/Resources <<
+/Font 13 0 R
+/ProcSet [ /ImageB /ImageC /ImageI /PDF /Text ]
+>>
+/Rotate 0
+/Trans <<
+>>
+/Type /Page
+/Parent 2 0 R
+>>
+endobj
+12 0 obj
+[ 15 0 R 16 0 R ]
+endobj
+13 0 obj
+<<
+/F1 14 0 R
+/F1-0 8 0 R
+>>
+endobj
+14 0 obj
+<<
+/BaseFont /Helvetica
+/Encoding /WinAnsiEncoding
+/Name /F1
+/Subtype /Type1
+/Type /Font
+>>
+endobj
+15 0 obj
+<<
+/Length 170
+>>
+stream
+q
+1 0.0 0.0 1 -10 -10 cm
+10 10 297.84 210 re
+W
+n
+q
+1 0.0 0.0 1 10 10 cm
+1 0 0 1 0 0 cm
+BT
+/F1-0 12 Tf
+14.4 TL
+ET
+1 0 0 rg
+n
+56.69291 56.69291 141.7323 28.34646 re
+B*
+Q
+Q
+
+endstream
+endobj
+16 0 obj
+<<
+/Length 106
+>>
+stream
+q
+1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
+q
+1 0 0 1 37.10551 140.9669 cm
+q
+1 0 0 1 0 5.669531 cm
+Q
+Q
+
+
+Q
+
+endstream
+endobj
+xref
+0 17
+0000000000 65535 f
+0000000015 00000 n
+0000000054 00000 n
+0000000120 00000 n
+0000000169 00000 n
+0000000361 00000 n
+0000000393 00000 n
+0000000436 00000 n
+0000000543 00000 n
+0000000650 00000 n
+0000000871 00000 n
+0000001029 00000 n
+0000001224 00000 n
+0000001258 00000 n
+0000001303 00000 n
+0000001411 00000 n
+0000001633 00000 n
+trailer
+<<
+/Size 17
+/Root 3 0 R
+/Info 1 0 R
+>>
+startxref
+1791
+%%EOF
diff --git a/src/tests/plugins/badges/assets/bg-rotated/bg.pdf b/src/tests/plugins/badges/assets/bg-rotated/bg.pdf
new file mode 100644
index 000000000..6be972dea
Binary files /dev/null and b/src/tests/plugins/badges/assets/bg-rotated/bg.pdf differ
diff --git a/src/tests/plugins/badges/assets/bg-rotated/expected.pdf b/src/tests/plugins/badges/assets/bg-rotated/expected.pdf
new file mode 100644
index 000000000..0e8ee99c9
--- /dev/null
+++ b/src/tests/plugins/badges/assets/bg-rotated/expected.pdf
@@ -0,0 +1,216 @@
+%PDF-1.7
+%âãÏÓ
+1 0 obj
+<<
+/Producer (pypdf)
+>>
+endobj
+2 0 obj
+<<
+/Type /Pages
+/Count 2
+/Kids [ 4 0 R 11 0 R ]
+>>
+endobj
+3 0 obj
+<<
+/Type /Catalog
+/Pages 2 0 R
+>>
+endobj
+4 0 obj
+<<
+/Contents 5 0 R
+/MediaBox [ 0 0 210 297.84 ]
+/Resources <<
+/Font 6 0 R
+/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
+/ExtGState <<
+/GS0 8 0 R
+>>
+>>
+/Rotate 0
+/Trans <<
+>>
+/Type /Page
+/Parent 2 0 R
+>>
+endobj
+5 0 obj
+[ 9 0 R 10 0 R ]
+endobj
+6 0 obj
+<<
+/F1 7 0 R
+>>
+endobj
+7 0 obj
+<<
+/BaseFont /Helvetica
+/Encoding /WinAnsiEncoding
+/Name /F1
+/Subtype /Type1
+/Type /Font
+>>
+endobj
+8 0 obj
+<<
+/Type /ExtGState
+/OP false
+/op false
+/OPM 1
+>>
+endobj
+9 0 obj
+<<
+/Length 287
+>>
+stream
+q
+0.00000000000000006123234 -1 1 0.00000000000000006123234 0.0 297.84 cm
+0.0 0.0 297.84 210 re
+W
+n
+/GS0 gs
+0.925811 0.881331 0.538643 rg
+0 0.23622 297.638 209.764 re
+f*
+0.476631 0.756542 0.260899 rg
+0 0.23622 297.638 209.764 re
+292.638 205 m
+5 205 l
+5 5.23622 l
+292.638 5.23622 l
+h
+f*
+Q
+
+endstream
+endobj
+10 0 obj
+<<
+/Length 106
+>>
+stream
+q
+1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
+q
+1 0 0 1 37.10551 140.9669 cm
+q
+1 0 0 1 0 5.669531 cm
+Q
+Q
+
+
+Q
+
+endstream
+endobj
+11 0 obj
+<<
+/Contents 12 0 R
+/MediaBox [ 0 0 210 297.84 ]
+/Resources <<
+/Font 13 0 R
+/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
+/ExtGState <<
+/GS0 8 0 R
+>>
+>>
+/Rotate 0
+/Trans <<
+>>
+/Type /Page
+/Parent 2 0 R
+>>
+endobj
+12 0 obj
+[ 15 0 R 16 0 R ]
+endobj
+13 0 obj
+<<
+/F1 14 0 R
+>>
+endobj
+14 0 obj
+<<
+/BaseFont /Helvetica
+/Encoding /WinAnsiEncoding
+/Name /F1
+/Subtype /Type1
+/Type /Font
+>>
+endobj
+15 0 obj
+<<
+/Length 287
+>>
+stream
+q
+0.00000000000000006123234 -1 1 0.00000000000000006123234 0.0 297.84 cm
+0.0 0.0 297.84 210 re
+W
+n
+/GS0 gs
+0.925811 0.881331 0.538643 rg
+0 0.23622 297.638 209.764 re
+f*
+0.476631 0.756542 0.260899 rg
+0 0.23622 297.638 209.764 re
+292.638 205 m
+5 205 l
+5 5.23622 l
+292.638 5.23622 l
+h
+f*
+Q
+
+endstream
+endobj
+16 0 obj
+<<
+/Length 106
+>>
+stream
+q
+1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
+q
+1 0 0 1 37.10551 140.9669 cm
+q
+1 0 0 1 0 5.669531 cm
+Q
+Q
+
+
+Q
+
+endstream
+endobj
+xref
+0 17
+0000000000 65535 f
+0000000015 00000 n
+0000000054 00000 n
+0000000120 00000 n
+0000000169 00000 n
+0000000389 00000 n
+0000000421 00000 n
+0000000452 00000 n
+0000000559 00000 n
+0000000624 00000 n
+0000000962 00000 n
+0000001120 00000 n
+0000001343 00000 n
+0000001377 00000 n
+0000001410 00000 n
+0000001518 00000 n
+0000001857 00000 n
+trailer
+<<
+/Size 17
+/Root 3 0 R
+/Info 1 0 R
+>>
+startxref
+2015
+%%EOF
diff --git a/src/tests/plugins/badges/test_pdf.py b/src/tests/plugins/badges/test_pdf.py
index 8dcdc8554..c8b9b696a 100644
--- a/src/tests/plugins/badges/test_pdf.py
+++ b/src/tests/plugins/badges/test_pdf.py
@@ -32,13 +32,18 @@
# 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.
+import logging
from datetime import timedelta
from decimal import Decimal
from io import BytesIO
+from pathlib import Path
+import pypdfium2
import pytest
+from django.core.files import File
from django.utils.timezone import now
from django_scopes import scope
+from PIL import ImageChops
from pypdf import PdfReader
from pretix.base.models import (
@@ -118,3 +123,53 @@ def test_generate_pdf_multi(env):
assert ftype == 'application/pdf'
pdf = PdfReader(BytesIO(buf))
assert len(pdf.pages) == 1
+
+
+def asset_path(name):
+ return Path(__file__).parent / "assets" / name
+
+
+def compare_pdfs(pdf_dir: Path, inp_a: Path | bytes, inp_b: Path | bytes):
+ logging.info(f"Comparing pdfs, writing files to {pdf_dir}")
+
+ pdf_a = pypdfium2.PdfDocument(inp_a)
+ pdf_b = pypdfium2.PdfDocument(inp_b)
+
+ pdf_a.save(pdf_dir / "a.pdf")
+ pdf_b.save(pdf_dir / "b.pdf")
+
+ assert len(pdf_a) == len(pdf_b)
+
+ for i, (page_a, page_b) in enumerate(zip(pdf_a, pdf_b)):
+ render_a = page_a.render()
+ render_b = page_b.render()
+ assert render_a.height == render_b.height
+ assert render_a.width == render_b.width
+
+ diff = ImageChops.difference(render_a.to_pil(), render_b.to_pil())
+ if diff.getbbox():
+ diff.save(pdf_dir / f"{i}.png")
+ assert not diff.getbbox(), f"Page {i} differs."
+
+
+@pytest.mark.django_db
+@pytest.mark.parametrize("case", [
+ "bg-rotated",
+ "bg-mediabox-offset",
+ "bg-cropbox"
+])
+def test_generate_pdf_weird_bgs(pdf_dir, env, case):
+ event, order, shirt = env
+ asset_folder = asset_path(case)
+ with open(asset_folder / "bg.pdf", 'rb') as fi:
+ event.badge_layouts.create(name="Default", default=True, background=File(fi, name="test.pdf"))
+ e = BadgeExporter(event, organizer=event.organizer)
+ fname, ftype, buf = e.render({
+ 'items': [shirt.pk],
+ 'rendering': 'one',
+ 'include_pending': True
+ })
+ assert ftype == 'application/pdf'
+ assert buf
+
+ compare_pdfs(pdf_dir, buf, asset_folder / "expected.pdf")
diff --git a/src/tests/plugins/banktransfer/test_actions.py b/src/tests/plugins/banktransfer/test_actions.py
index fc9cfc164..42555484a 100644
--- a/src/tests/plugins/banktransfer/test_actions.py
+++ b/src/tests/plugins/banktransfer/test_actions.py
@@ -23,6 +23,7 @@ import json
from datetime import timedelta
import pytest
+from django.dispatch import receiver
from django.utils.timezone import now
from django_scopes import scopes_disabled
@@ -30,6 +31,7 @@ from pretix.base.models import (
Event, Item, Order, OrderPayment, OrderPosition, OrderRefund, Organizer,
Quota, Team, User,
)
+from pretix.base.signals import order_paid
from pretix.plugins.banktransfer.models import BankImportJob, BankTransaction
@@ -38,7 +40,7 @@ def env():
o = Organizer.objects.create(name='Dummy', slug='dummy', plugins='pretix.plugins.banktransfer')
event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
- date_from=now(), plugins='pretix.plugins.banktransfer'
+ date_from=now(), plugins='pretix.plugins.banktransfer,testplugin'
)
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
t = Team.objects.create(organizer=event.organizer, all_event_permissions=True)
@@ -376,3 +378,44 @@ def test_retry_refund_complete(env, client):
assert ref.amount == 23
assert ref.payment is None
assert ref.state == OrderRefund.REFUND_STATE_DONE
+
+
+def _receiver_with_fake_plugin_name(signal, obj, plugin_name):
+ class App:
+ name = plugin_name
+
+ class PretixPluginMeta:
+ pass
+
+ obj.__mocked_app = App
+ receiver(signal)(obj)
+
+
+@pytest.mark.django_db
+@scopes_disabled()
+def test_assign_order_atomic(env, client):
+ job = BankImportJob.objects.create(event=env[0])
+ trans = BankTransaction.objects.create(event=env[0], import_job=job, payer='Foo',
+ state=BankTransaction.STATE_NOMATCH,
+ amount=23, date='unknown')
+
+ def fail_order_paid(*args, **kwargs):
+ raise Exception()
+ _receiver_with_fake_plugin_name(order_paid, fail_order_paid, 'testplugin')
+
+ assert env[2].payments.count() == 0
+
+ client.login(email='dummy@dummy.dummy', password='dummy')
+ try:
+ r = json.loads(client.post('/control/event/{}/{}/banktransfer/action/'.format(env[0].organizer.slug, env[0].slug), {
+ 'action_{}'.format(trans.pk): 'assign:{}'.format(env[2].code),
+ }).content.decode('utf-8'))
+ assert r['status'] == 'ok'
+ except:
+ pass
+
+ trans.refresh_from_db()
+ assert trans.state == BankTransaction.STATE_NOMATCH
+ env[2].refresh_from_db()
+ assert env[2].status == Order.STATUS_PENDING
+ assert env[2].payments.count() == 0
diff --git a/src/tests/plugins/banktransfer/test_import.py b/src/tests/plugins/banktransfer/test_import.py
index 508d6198b..4253e2715 100644
--- a/src/tests/plugins/banktransfer/test_import.py
+++ b/src/tests/plugins/banktransfer/test_import.py
@@ -56,7 +56,7 @@ def env():
o = Organizer.objects.create(name='Dummy', slug='dummy', plugins='pretix.plugins.banktransfer')
event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
- date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
+ date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal2'
)
event.settings.invoice_numbers_prefix = 'INV-'
event.settings.invoice_numbers_counter_length = 3
@@ -843,7 +843,7 @@ def test_event_name_prefix_contains_dash(env, orga_job):
for slug in slugs:
event = Event.objects.create(
organizer=event.organizer, name=slug.upper(), slug=slug,
- date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
+ date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal2'
)
with scopes_disabled():
o1.event = Event.objects.get(slug="dummy2345")
@@ -871,7 +871,7 @@ def test_event_name_prefix_multiple_dashes(env, orga_job):
for slug in slugs:
event = Event.objects.create(
organizer=event.organizer, name=slug.upper(), slug=slug,
- date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
+ date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal2'
)
with scopes_disabled():
diff --git a/src/tests/plugins/banktransfer/test_refund.py b/src/tests/plugins/banktransfer/test_refund.py
index f970f0f3f..41c39cd85 100644
--- a/src/tests/plugins/banktransfer/test_refund.py
+++ b/src/tests/plugins/banktransfer/test_refund.py
@@ -37,7 +37,7 @@ def env():
o = Organizer.objects.create(name='Dummy', slug='dummy', plugins='pretix.plugins.banktransfer')
event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
- date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
+ date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal2'
)
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
t = Team.objects.create(organizer=event.organizer, all_event_permissions=True)
diff --git a/src/tests/plugins/banktransfer/test_refund_export.py b/src/tests/plugins/banktransfer/test_refund_export.py
index b6c5cf255..7eb311c5a 100644
--- a/src/tests/plugins/banktransfer/test_refund_export.py
+++ b/src/tests/plugins/banktransfer/test_refund_export.py
@@ -38,7 +38,7 @@ def env():
o = Organizer.objects.create(name='Dummy', slug='dummy', plugins='pretix.plugins.banktransfer')
event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
- date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
+ date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal2'
)
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
t = Team.objects.create(organizer=event.organizer, all_event_permissions=True)
@@ -69,7 +69,7 @@ def env():
def refund_huf(env):
event = Event.objects.create(
organizer=env[0].organizer, name='Dummy', slug='dummy2', currency='HUF',
- date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal'
+ date_from=now(), plugins='pretix.plugins.banktransfer,pretix.plugins.paypal2'
)
order = Order.objects.create(
code='HUFFY', event=event, email='admin@localhost',
diff --git a/src/tests/plugins/paypal/test_checkout.py b/src/tests/plugins/paypal/test_checkout.py
deleted file mode 100644
index 9eb068736..000000000
--- a/src/tests/plugins/paypal/test_checkout.py
+++ /dev/null
@@ -1,93 +0,0 @@
-#
-# This file is part of pretix (Community Edition).
-#
-# Copyright (C) 2014-2020 Raphael Michel and contributors
-# Copyright (C) 2020-today pretix 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 .
-#
-# 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
-# .
-#
-
-# 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 .
-#
-# 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 .
-#
-# This file contains Apache-licensed contributions copyrighted by: Tobias Kunze
-#
-# 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.
-
-import datetime
-
-import pytest
-from django.utils.timezone import now
-
-from pretix.base.models import (
- CartPosition, Event, Item, ItemCategory, Organizer, Quota,
-)
-from pretix.testutils.sessions import add_cart_session, get_cart_session_key
-
-
-@pytest.fixture
-def env(client):
- orga = Organizer.objects.create(name='CCC', slug='ccc')
- event = Event.objects.create(
- organizer=orga, name='30C3', slug='30c3',
- date_from=datetime.datetime(now().year + 1, 12, 26, tzinfo=datetime.timezone.utc),
- plugins='pretix.plugins.paypal',
- live=True
- )
- category = ItemCategory.objects.create(event=event, name="Everything", position=0)
- quota_tickets = Quota.objects.create(event=event, name='Tickets', size=5)
- ticket = Item.objects.create(event=event, name='Early-bird ticket',
- category=category, default_price=23, admission=True)
- quota_tickets.items.add(ticket)
- event.settings.set('attendee_names_asked', False)
- event.settings.set('payment_paypal__enabled', True)
- event.settings.set('payment_paypal__fee_abs', 3)
- event.settings.set('payment_paypal_endpoint', 'sandbox')
- event.settings.set('payment_paypal_client_id', '12345')
- event.settings.set('payment_paypal_secret', '12345')
- add_cart_session(client, event, {'email': 'admin@localhost'})
- return client, ticket
-
-
-@pytest.mark.django_db
-def test_payment(env, monkeypatch):
- def create_payment(self, request, payment):
- assert payment['intent'] == 'sale'
- assert payment['transactions'][0]['amount']['currency'] == 'EUR'
- assert payment['transactions'][0]['amount']['total'] == '26.00'
- create_payment.called = True
- return 'https://approve.url'
- monkeypatch.setattr("pretix.plugins.paypal.payment.Paypal._create_payment", create_payment)
-
- client, ticket = env
- session_key = get_cart_session_key(client, ticket.event)
- CartPosition.objects.create(
- event=ticket.event, cart_id=session_key, item=ticket,
- price=23, expires=now() + datetime.timedelta(minutes=10)
- )
- client.get('/%s/%s/checkout/payment/' % (ticket.event.organizer.slug, ticket.event.slug), follow=True)
- client.post('/%s/%s/checkout/questions/' % (ticket.event.organizer.slug, ticket.event.slug), {
- 'email': 'admin@localhost'
- }, follow=True)
- response = client.post('/%s/%s/checkout/payment/' % (ticket.event.organizer.slug, ticket.event.slug), {
- 'payment': 'paypal'
- })
- assert response['Location'] == 'https://approve.url'
diff --git a/src/tests/plugins/paypal/test_settings.py b/src/tests/plugins/paypal/test_settings.py
deleted file mode 100644
index e6b969c54..000000000
--- a/src/tests/plugins/paypal/test_settings.py
+++ /dev/null
@@ -1,67 +0,0 @@
-#
-# This file is part of pretix (Community Edition).
-#
-# Copyright (C) 2014-2020 Raphael Michel and contributors
-# Copyright (C) 2020-today pretix 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 .
-#
-# 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
-# .
-#
-
-# 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 .
-#
-# 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 .
-#
-# This file contains Apache-licensed contributions copyrighted by: Tobias Kunze
-#
-# 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.
-
-import datetime
-
-import pytest
-
-from pretix.base.models import Event, Organizer, Team, User
-
-
-@pytest.fixture
-def env(client):
- orga = Organizer.objects.create(name='CCC', slug='ccc')
- event = Event.objects.create(
- organizer=orga, name='30C3', slug='30c3',
- date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc),
- plugins='pretix.plugins.paypal',
- live=True
- )
- event.settings.set('attendee_names_asked', False)
- event.settings.set('payment_paypal__enabled', True)
- user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
- t = Team.objects.create(organizer=event.organizer, all_event_permissions=True)
- t.members.add(user)
- t.limit_events.add(event)
- client.force_login(user)
- return client, event
-
-
-@pytest.mark.django_db
-def test_settings(env):
- client, event = env
- response = client.get('/control/event/%s/%s/settings/payment/paypal' % (event.organizer.slug, event.slug),
- follow=True)
- assert response.status_code == 200
- assert 'paypal__enabled' in response.rendered_content
diff --git a/src/tests/plugins/paypal/test_webhook.py b/src/tests/plugins/paypal/test_webhook.py
deleted file mode 100644
index c6dd2223c..000000000
--- a/src/tests/plugins/paypal/test_webhook.py
+++ /dev/null
@@ -1,391 +0,0 @@
-#
-# This file is part of pretix (Community Edition).
-#
-# Copyright (C) 2014-2020 Raphael Michel and contributors
-# Copyright (C) 2020-today pretix 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 .
-#
-# 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
-# .
-#
-import json
-from datetime import timedelta
-from decimal import Decimal
-
-import pytest
-from django.utils.timezone import now
-from django_scopes import scopes_disabled
-
-from pretix.base.models import (
- Event, Order, OrderPayment, OrderRefund, Organizer, Team, User,
-)
-from pretix.plugins.paypal.models import ReferencedPayPalObject
-
-
-@pytest.fixture
-def env():
- user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
- o = Organizer.objects.create(name='Dummy', slug='dummy')
- event = Event.objects.create(
- organizer=o, name='Dummy', slug='dummy', plugins='pretix.plugins.paypal',
- date_from=now(), live=True
- )
- t = Team.objects.create(organizer=event.organizer, all_event_permissions=True)
- t.members.add(user)
- t.limit_events.add(event)
- o1 = Order.objects.create(
- code='FOOBAR', event=event, email='dummy@dummy.test',
- status=Order.STATUS_PAID,
- datetime=now(), expires=now() + timedelta(days=10),
- total=Decimal('13.37'),
- sales_channel=o.sales_channels.get(identifier="web"),
- )
- o1.payments.create(
- amount=o1.total,
- provider='paypal',
- state=OrderPayment.PAYMENT_STATE_CONFIRMED,
- info=json.dumps({
- "id": "PAY-5YK922393D847794YKER7MUI",
- "create_time": "2013-02-19T22:01:53Z",
- "update_time": "2013-02-19T22:01:55Z",
- "state": "approved",
- "intent": "sale",
- "payer": {
- "payment_method": "credit_card",
- "funding_instruments": [
- {
- "credit_card": {
- "type": "mastercard",
- "number": "xxxxxxxxxxxx5559",
- "expire_month": 2,
- "expire_year": 2018,
- "first_name": "Betsy",
- "last_name": "Buyer"
- }
- }
- ]
- },
- "transactions": [
- {
- "amount": {
- "total": "7.47",
- "currency": "USD",
- "details": {
- "subtotal": "7.47"
- }
- },
- "description": "This is the payment transaction description.",
- "note_to_payer": "Contact us for any questions on your order.",
- "related_resources": [
- {
- "sale": {
- "id": "36C38912MN9658832",
- "create_time": "2013-02-19T22:01:53Z",
- "update_time": "2013-02-19T22:01:55Z",
- "state": "completed",
- "amount": {
- "total": "7.47",
- "currency": "USD"
- },
- "protection_eligibility": "ELIGIBLE",
- "protection_eligibility_type": "ITEM_NOT_RECEIVED_ELIGIBLE",
- "transaction_fee": {
- "value": "1.75",
- "currency": "USD"
- },
- "parent_payment": "PAY-5YK922393D847794YKER7MUI",
- "links": [
- {
- "href": "https://api.paypal.com/v1/payments/sale/36C38912MN9658832",
- "rel": "self",
- "method": "GET"
- },
- {
- "href": "https://api.paypal.com/v1/payments/sale/36C38912MN9658832/refund",
- "rel": "refund",
- "method": "POST"
- },
- {
- "href":
- "https://api.paypal.com/v1/payments/payment/PAY-5YK922393D847794YKER7MUI",
- "rel": "parent_payment",
- "method": "GET"
- }
- ]
- }
- }
- ]
- }
- ],
- "links": [
- {
- "href": "https://api.paypal.com/v1/payments/payment/PAY-5YK922393D847794YKER7MUI",
- "rel": "self",
- "method": "GET"
- }
- ]
-
- })
- )
- return event, o1
-
-
-def get_test_charge(order: Order):
- return {
- "id": "36C38912MN9658832",
- "create_time": "2013-02-19T22:01:53Z",
- "update_time": "2013-02-19T22:01:55Z",
- "state": "completed",
- "amount": {
- "total": "7.47",
- "currency": "USD"
- },
- "protection_eligibility": "ELIGIBLE",
- "protection_eligibility_type": "ITEM_NOT_RECEIVED_ELIGIBLE,UNAUTHORIZED_PAYMENT_ELIGIBLE",
- "transaction_fee": {
- "value": "1.75",
- "currency": "USD"
- },
- "parent_payment": "PAY-5YK922393D847794YKER7MUI",
- "links": [
- {
- "href": "https://api.paypal.com/v1/payments/sale/36C38912MN9658832",
- "rel": "self",
- "method": "GET"
- },
- {
- "href": "https://api.paypal.com/v1/payments/sale/36C38912MN9658832/refund",
- "rel": "refund",
- "method": "POST"
- },
- {
- "href": "https://api.paypal.com/v1/payments/payment/PAY-5YK922393D847794YKER7MUI",
- "rel": "parent_payment",
- "method": "GET"
- }
- ]
- }
-
-
-def get_test_refund(order: Order):
- return {
- 'refund_from_received_amount': {'value': '13.30', 'currency': 'EUR'},
- 'amount': {'total': '13.37', 'currency': 'EUR'},
- 'sale_id': '1G495778AR8401726',
- 'update_time': '2018-07-24T07:50:07Z',
- 'total_refunded_amount': {'value': '13.37', 'currency': 'EUR'},
- 'refund_reason_code': 'REFUND',
- 'invoice_number': 'Test',
- 'parent_payment': 'PAY-0UB50445HE155450FLNLNMUY',
- 'state': 'completed',
- 'create_time': '2018-07-24T07:50:07Z',
- 'refund_from_transaction_fee': {'value': '0.07', 'currency': 'EUR'},
- 'id': '93M41501U3542574L',
- 'refund_to_payer': {'value': '13.37', 'currency': 'EUR'},
- 'links': [
- {'method': 'GET', 'rel': 'self',
- 'href': 'https://api.sandbox.paypal.com/v1/payments/refund/93M41501U3542574L'},
- {'method': 'GET',
- 'rel': 'parent_payment',
- 'href': 'https://api.sandbox.paypal.com/v1/payments/payment/PAY-0UB50445HE155450FLNLNMUY'},
- {'method': 'GET', 'rel': 'sale',
- 'href': 'https://api.sandbox.paypal.com/v1/payments/sale/1G495778AR8401726'}
- ]
- }
-
-
-@pytest.mark.django_db
-def test_webhook_all_good(env, client, monkeypatch):
- charge = get_test_charge(env[1])
- monkeypatch.setattr("paypalrestsdk.Sale.find", lambda *args: charge)
- monkeypatch.setattr("pretix.plugins.paypal.payment.Paypal.init_api", lambda *args: None)
-
- client.post('/dummy/dummy/paypal/webhook/', json.dumps(
- {
- "id": "WH-2WR32451HC0233532-67976317FL4543714",
- "create_time": "2014-10-23T17:23:52Z",
- "resource_type": "sale",
- "event_type": "PAYMENT.SALE.COMPLETED",
- "summary": "A successful sale payment was made for $ 0.48 USD",
- "resource": {
- "amount": {
- "total": "-0.01",
- "currency": "USD"
- },
- "id": "36C38912MN9658832",
- "parent_payment": "PAY-5YK922393D847794YKER7MUI",
- "update_time": "2014-10-31T15:41:51Z",
- "state": "completed",
- "create_time": "2014-10-31T15:41:51Z",
- "links": [],
- "sale_id": "9T0916710M1105906"
- },
- "links": [],
- "event_version": "1.0"
- }
- ), content_type='application_json')
-
- order = env[1]
- order.refresh_from_db()
- assert order.status == Order.STATUS_PAID
-
-
-@pytest.mark.django_db
-def test_webhook_mark_paid(env, client, monkeypatch):
- order = env[1]
- order.status = Order.STATUS_PENDING
- order.save()
- with scopes_disabled():
- order.payments.update(state=OrderPayment.PAYMENT_STATE_PENDING)
-
- charge = get_test_charge(env[1])
- monkeypatch.setattr("paypalrestsdk.Sale.find", lambda *args: charge)
- monkeypatch.setattr("pretix.plugins.paypal.payment.Paypal.init_api", lambda *args: None)
- ReferencedPayPalObject.objects.create(order=order, reference="PAY-5YK922393D847794YKER7MUI")
-
- client.post('/_paypal/webhook/', json.dumps(
- {
- "id": "WH-2WR32451HC0233532-67976317FL4543714",
- "create_time": "2014-10-23T17:23:52Z",
- "resource_type": "sale",
- "event_type": "PAYMENT.SALE.COMPLETED",
- "summary": "A successful sale payment was made for $ 0.48 USD",
- "resource": {
- "amount": {
- "total": "-0.01",
- "currency": "USD"
- },
- "id": "36C38912MN9658832",
- "parent_payment": "PAY-5YK922393D847794YKER7MUI",
- "update_time": "2014-10-31T15:41:51Z",
- "state": "completed",
- "create_time": "2014-10-31T15:41:51Z",
- "links": [],
- "sale_id": "9T0916710M1105906"
- },
- "links": [],
- "event_version": "1.0"
- }
- ), content_type='application_json')
-
- order.refresh_from_db()
- assert order.status == Order.STATUS_PAID
-
-
-@pytest.mark.django_db
-def test_webhook_refund1(env, client, monkeypatch):
- order = env[1]
- charge = get_test_charge(env[1])
- charge['state'] = 'refunded'
- refund = get_test_refund(env[1])
-
- monkeypatch.setattr("paypalrestsdk.Sale.find", lambda *args: charge)
- monkeypatch.setattr("paypalrestsdk.Refund.find", lambda *args: refund)
- monkeypatch.setattr("pretix.plugins.paypal.payment.Paypal.init_api", lambda *args: None)
- ReferencedPayPalObject.objects.create(order=order, reference="PAY-5YK922393D847794YKER7MUI")
-
- client.post('/_paypal/webhook/', json.dumps(
- {
- # Sample obtained in a sandbox webhook
- "id": "WH-9K829080KA1622327-31011919VC6498738",
- "create_time": "2017-01-15T20:15:36Z",
- "resource_type": "refund",
- "event_type": "PAYMENT.SALE.REFUNDED",
- "summary": "A EUR 255.41 EUR sale payment was refunded",
- "resource": {
- "amount": {
- "total": "255.41",
- "currency": "EUR"
- },
- "id": "75S46770PP192124D",
- "parent_payment": "PAY-5YK922393D847794YKER7MUI",
- "update_time": "2017-01-15T20:15:06Z",
- "create_time": "2017-01-15T20:14:29Z",
- "state": "completed",
- "links": [],
- "refund_to_payer": {
- "value": "255.41",
- "currency": "EUR"
- },
- "invoice_number": "",
- "refund_reason_code": "REFUND",
- "sale_id": "9T0916710M1105906"
- },
- "links": [],
- "event_version": "1.0"
- }
- ), content_type='application_json')
-
- order = env[1]
- order.refresh_from_db()
- assert order.status == Order.STATUS_PAID
-
- with scopes_disabled():
- r = order.refunds.first()
- assert r.provider == 'paypal'
- assert r.amount == order.total
- assert r.payment == order.payments.first()
- assert r.state == OrderRefund.REFUND_STATE_EXTERNAL
- assert r.source == OrderRefund.REFUND_SOURCE_EXTERNAL
-
-
-@pytest.mark.django_db
-def test_webhook_refund2(env, client, monkeypatch):
- order = env[1]
- charge = get_test_charge(env[1])
- charge['state'] = 'refunded'
- refund = get_test_refund(env[1])
-
- monkeypatch.setattr("paypalrestsdk.Sale.find", lambda *args: charge)
- monkeypatch.setattr("paypalrestsdk.Refund.find", lambda *args: refund)
- monkeypatch.setattr("pretix.plugins.paypal.payment.Paypal.init_api", lambda *args: None)
- ReferencedPayPalObject.objects.create(order=order, reference="PAY-5YK922393D847794YKER7MUI")
-
- client.post('/_paypal/webhook/', json.dumps(
- {
- # Sample obtained in the webhook simulator
- "id": "WH-2N242548W9943490U-1JU23391CS4765624",
- "create_time": "2014-10-31T15:42:24Z",
- "resource_type": "refund",
- "event_type": "PAYMENT.SALE.REFUNDED",
- "summary": "A 0.01 USD sale payment was refunded",
- "resource": {
- "amount": {
- "total": "-0.01",
- "currency": "USD"
- },
- "id": "36C38912MN9658832",
- "parent_payment": "PAY-5YK922393D847794YKER7MUI",
- "update_time": "2014-10-31T15:41:51Z",
- "state": "completed",
- "create_time": "2014-10-31T15:41:51Z",
- "links": [],
- "sale_id": "9T0916710M1105906"
- },
- "links": [],
- "event_version": "1.0"
- }
- ), content_type='application_json')
-
- order = env[1]
- order.refresh_from_db()
- assert order.status == Order.STATUS_PAID
-
- with scopes_disabled():
- r = order.refunds.first()
- assert r.provider == 'paypal'
- assert r.amount == order.total
- assert r.payment == order.payments.first()
- assert r.state == OrderRefund.REFUND_STATE_EXTERNAL
- assert r.source == OrderRefund.REFUND_SOURCE_EXTERNAL
diff --git a/src/tests/plugins/paypal2/test_webhook.py b/src/tests/plugins/paypal2/test_webhook.py
index b1355018c..0c0f24be9 100644
--- a/src/tests/plugins/paypal2/test_webhook.py
+++ b/src/tests/plugins/paypal2/test_webhook.py
@@ -22,6 +22,7 @@
import json
from datetime import timedelta
from decimal import Decimal
+from unittest.mock import MagicMock
import pytest
from django.utils.timezone import now
@@ -31,7 +32,7 @@ from paypalhttp.http_response import Result
from pretix.base.models import (
Event, Order, OrderPayment, OrderRefund, Organizer, Team, User,
)
-from pretix.plugins.paypal.models import ReferencedPayPalObject
+from pretix.plugins.paypal2.models import ReferencedPayPalObject
@pytest.fixture
@@ -243,6 +244,85 @@ def get_test_refund():
}
+def get_test_order_review_pending():
+ return {'id': '806440346Y391300T',
+ 'intent': 'CAPTURE',
+ 'status': 'COMPLETED',
+ 'purchase_units': [{'reference_id': 'default',
+ 'amount': {'currency_code': 'EUR', 'value': '43.59'},
+ 'payee': {'email_address': 'dummy-facilitator@dummy.dummy',
+ 'merchant_id': 'G6R2B9YXADKWW'},
+ 'description': 'Order JWJGC for PayPal v2',
+ 'custom_id': 'Order PAYPALV2-JWJGC',
+ 'soft_descriptor': 'MARTINFACIL',
+ 'payments': {'captures': [{'id': '22A4162004478570J',
+ 'status': 'PENDING',
+ 'status_details': {
+ 'reason': 'PENDING_REVIEW'
+ },
+ 'amount': {'currency_code': 'EUR', 'value': '43.59'},
+ 'final_capture': True,
+ 'disbursement_mode': 'INSTANT',
+ 'seller_protection': {'status': 'ELIGIBLE',
+ 'dispute_categories': [
+ 'ITEM_NOT_RECEIVED',
+ 'UNAUTHORIZED_TRANSACTION']},
+ 'seller_receivable_breakdown': {
+ 'gross_amount': {'currency_code': 'EUR',
+ 'value': '43.59'},
+ 'paypal_fee': {'currency_code': 'EUR', 'value': '1.18'},
+ 'net_amount': {'currency_code': 'EUR',
+ 'value': '42.41'}},
+ 'custom_id': 'Order PAYPALV2-JWJGC',
+ 'links': [{
+ 'href': 'https://api.sandbox.paypal.com/v2/payments/captures/22A4162004478570J',
+ 'rel': 'self',
+ 'method': 'GET'},
+ {
+ 'href': 'https://api.sandbox.paypal.com/v2/payments/captures/22A4162004478570J/refund',
+ 'rel': 'refund',
+ 'method': 'POST'},
+ {
+ 'href': 'https://api.sandbox.paypal.com/v2/checkout/orders/806440346Y391300T',
+ 'rel': 'up',
+ 'method': 'GET'}],
+ 'create_time': '2022-04-28T12:00:22Z',
+ 'update_time': '2022-04-28T12:00:22Z'}]}}],
+ 'payer': {'name': {'given_name': 'test', 'surname': 'buyer'},
+ 'email_address': 'dummy@dummy.dummy',
+ 'payer_id': 'Q739JNKWH67HE',
+ 'address': {'country_code': 'DE'}},
+ 'create_time': '2022-04-28T11:59:59Z',
+ 'update_time': '2022-04-28T12:00:22Z',
+ 'links': [{'href': 'https://api.sandbox.paypal.com/v2/checkout/orders/806440346Y391300T',
+ 'rel': 'self',
+ 'method': 'GET'}]}
+
+
+def get_test_empty_captures():
+ return {'id': '806440346Y391300T',
+ 'intent': 'CAPTURE',
+ 'status': 'COMPLETED',
+ 'purchase_units': [{'reference_id': 'default',
+ 'amount': {'currency_code': 'EUR', 'value': '43.59'},
+ 'payee': {'email_address': 'dummy-facilitator@dummy.dummy',
+ 'merchant_id': 'G6R2B9YXADKWW'},
+ 'description': 'Order JWJGC for PayPal v2',
+ 'custom_id': 'Order PAYPALV2-JWJGC',
+ 'soft_descriptor': 'MARTINFACIL',
+ 'payments': {'captures': []}
+ }],
+ 'payer': {'name': {'given_name': 'test', 'surname': 'buyer'},
+ 'email_address': 'dummy@dummy.dummy',
+ 'payer_id': 'Q739JNKWH67HE',
+ 'address': {'country_code': 'DE'}},
+ 'create_time': '2022-04-28T11:59:59Z',
+ 'update_time': '2022-04-28T12:00:22Z',
+ 'links': [{'href': 'https://api.sandbox.paypal.com/v2/checkout/orders/806440346Y391300T',
+ 'rel': 'self',
+ 'method': 'GET'}]}
+
+
class Object():
pass
@@ -400,6 +480,100 @@ def test_webhook_all_good(env, client, monkeypatch):
assert order.status == Order.STATUS_PAID
+@pytest.mark.django_db
+def test_webhook_empty_captures(env, client, monkeypatch):
+ order = env[1]
+ with scopes_disabled():
+ p = order.payments.first()
+ p.state = OrderPayment.PAYMENT_STATE_PENDING
+ p.save()
+ order.status = Order.STATUS_PENDING
+ order.save()
+
+ pp_order = Result(get_test_empty_captures())
+ monkeypatch.setattr("paypalcheckoutsdk.orders.OrdersGetRequest", lambda *args: pp_order)
+ monkeypatch.setattr("pretix.plugins.paypal2.payment.PaypalMethod.init_api", init_api)
+
+ with scopes_disabled():
+ ReferencedPayPalObject.objects.create(order=order, payment=order.payments.first(),
+ reference="806440346Y391300T")
+
+ client.post('/_paypal/webhook/', json.dumps(
+ {
+ "id": "WH-4T867178D0574904F-7TT11736YU643990P",
+ "create_time": "2022-04-28T12:00:37.077Z",
+ "resource_type": "checkout-order",
+ "event_type": "CHECKOUT.ORDER.COMPLETED",
+ "summary": "Checkout Order Completed",
+ "resource": {
+ "update_time": "2022-04-28T12:00:22Z",
+ "create_time": "2022-04-28T11:59:59Z",
+ "purchase_units": [
+ {
+ "reference_id": "default",
+ "amount": {
+ "currency_code": "EUR",
+ "value": "43.59"
+ },
+ "payee": {
+ "email_address": "dummy-facilitator@dummy.dummy",
+ "merchant_id": "G6R2B9YXADKWW"
+ },
+ "description": "Order JWJGC for PayPal v2",
+ "custom_id": "Order PAYPALV2-JWJGC",
+ "soft_descriptor": "MARTINFACIL",
+ "payments": {
+ "captures": []
+ }
+ }
+ ],
+ "links": [
+ {
+ "href": "https://api.sandbox.paypal.com/v2/checkout/orders/806440346Y391300T",
+ "rel": "self",
+ "method": "GET"
+ }
+ ],
+ "id": "806440346Y391300T",
+ "intent": "CAPTURE",
+ "payer": {
+ "name": {
+ "given_name": "test",
+ "surname": "buyer"
+ },
+ "email_address": "dummy@dummy.dummy",
+ "payer_id": "Q739JNKWH67HE",
+ "address": {
+ "country_code": "DE"
+ }
+ },
+ "status": "COMPLETED"
+ },
+ "status": "SUCCESS",
+ "links": [
+ {
+ "href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-4T867178D0574904F-7TT11736YU643990P",
+ "rel": "self",
+ "method": "GET",
+ "encType": "application/json"
+ },
+ {
+ "href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-4T867178D0574904F-7TT11736YU643990P/resend",
+ "rel": "resend",
+ "method": "POST",
+ "encType": "application/json"
+ }
+ ],
+ "event_version": "1.0",
+ "resource_version": "2.0"
+ }
+ ), content_type='application_json')
+
+ order = env[1]
+ order.refresh_from_db()
+ assert order.status == Order.STATUS_PENDING
+
+
@pytest.mark.django_db
def test_webhook_mark_paid(env, client, monkeypatch):
order = env[1]
@@ -409,7 +583,8 @@ def test_webhook_mark_paid(env, client, monkeypatch):
order.payments.update(state=OrderPayment.PAYMENT_STATE_PENDING)
pp_order = Result(get_test_order())
- monkeypatch.setattr("paypalcheckoutsdk.orders.OrdersGetRequest", lambda *args: pp_order)
+ mock_orders_get_request = MagicMock(return_value=pp_order)
+ monkeypatch.setattr("paypalcheckoutsdk.orders.OrdersGetRequest", mock_orders_get_request)
monkeypatch.setattr("pretix.plugins.paypal2.payment.PaypalMethod.init_api", init_api)
with scopes_disabled():
ReferencedPayPalObject.objects.create(order=order, payment=order.payments.first(),
@@ -497,7 +672,7 @@ def test_webhook_mark_paid(env, client, monkeypatch):
"resource_version": "2.0"
}
), content_type='application_json')
-
+ mock_orders_get_request.assert_called_once_with('806440346Y391300T')
order.refresh_from_db()
assert order.status == Order.STATUS_PAID
@@ -688,3 +863,154 @@ def test_webhook_refund2(env, client, monkeypatch):
assert r.payment == order.payments.first()
assert r.state == OrderRefund.REFUND_STATE_EXTERNAL
assert r.source == OrderRefund.REFUND_SOURCE_EXTERNAL
+
+
+@pytest.mark.django_db
+def test_webhook_pending_payment(env, client, monkeypatch):
+ order = env[1]
+ order.status = Order.STATUS_PENDING
+ order.save()
+ with scopes_disabled():
+ order.payments.update(state=OrderPayment.PAYMENT_STATE_CREATED)
+
+ pp_order = Result(get_test_order_review_pending())
+ mock_orders_get_request = MagicMock(return_value=pp_order)
+ monkeypatch.setattr("paypalcheckoutsdk.orders.OrdersGetRequest", mock_orders_get_request)
+ monkeypatch.setattr("pretix.plugins.paypal2.payment.PaypalMethod.init_api", init_api)
+ with scopes_disabled():
+ ReferencedPayPalObject.objects.create(order=order, payment=order.payments.first(),
+ reference="806440346Y391300T")
+
+ assert order.payments.first().state == OrderPayment.PAYMENT_STATE_CREATED
+
+ client.post('/_paypal/webhook/', json.dumps(
+ {
+ "id": "WH-0AH02875JL566842H-2AF03788XV8252724",
+ "create_time": "2022-04-28T12:00:37.077Z",
+ "resource_type": "capture",
+ "event_type": "PAYMENT.CAPTURE.PENDING",
+ "summary": "Payment pending for € 43.59 EUR",
+ "resource": {
+ "update_time": "2022-04-28T12:00:22Z",
+ "create_time": "2022-04-28T11:59:59Z",
+ "amount": {
+ "currency_code": "EUR",
+ "value": "43.59"
+ },
+ "custom_id": "Order PAYPALV2-JWJGC",
+ "final_capture": True,
+ "id": "22A4162004478570J",
+ "links": [
+ {
+ "href": "https://api.sandbox.paypal.com/v2/payments/captures/5M631111V9599860P",
+ "method": "GET",
+ "rel": "self"
+ },
+ {
+ "href": "https://api.sandbox.paypal.com/v2/payments/captures/5M631111V9599860P/refund",
+ "method": "POST",
+ "rel": "refund"
+ },
+ {
+ "href": "https://api.sandbox.paypal.com/v2/checkout/orders/806440346Y391300T",
+ "method": "GET",
+ "rel": "up"
+ }
+ ],
+ "payee": {
+ "email_address": "sb-ybfun52428692@business.example.com",
+ "merchant_id": "DLM8QKDR3CFZW"
+ },
+ "seller_protection": {
+ "status": "NOT_ELIGIBLE"
+ },
+ "status": "PENDING",
+ "status_details": {
+ "reason": "PENDING_REVIEW"
+ },
+ "supplementary_data": {
+ "related_ids": {
+ "order_id": "9L827155WD164573M"
+ }
+ }
+ },
+ "links": [
+ {
+ "href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-0AH02875JL566842H-2AF03788XV8252724",
+ "method": "GET",
+ "rel": "self"
+ },
+ {
+ "href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-0AH02875JL566842H-2AF03788XV8252724/resend",
+ "method": "POST",
+ "rel": "resend"
+ }
+ ],
+ "event_version": "1.0",
+ "resource_version": "2.0"
+ }
+ ), content_type='application_json')
+
+ order = env[1]
+ order.refresh_from_db()
+ with scopes_disabled():
+ assert order.payments.first().state == OrderPayment.PAYMENT_STATE_PENDING
+
+
+@pytest.mark.django_db
+def test_webhook_capture_declined(env, client, monkeypatch):
+ order = env[1]
+ order.status = Order.STATUS_PENDING
+ order.save()
+ with scopes_disabled():
+ order.payments.update(state=OrderPayment.PAYMENT_STATE_CREATED)
+
+ pp_order = Result(get_test_order_review_pending())
+ mock_orders_get_request = MagicMock(return_value=pp_order)
+ monkeypatch.setattr("paypalcheckoutsdk.orders.OrdersGetRequest", mock_orders_get_request)
+ monkeypatch.setattr("pretix.plugins.paypal2.payment.PaypalMethod.init_api", init_api)
+ with scopes_disabled():
+ ReferencedPayPalObject.objects.create(order=order, payment=order.payments.first(),
+ reference="806440346Y391300T")
+
+ assert order.payments.first().state == OrderPayment.PAYMENT_STATE_CREATED
+
+ client.post('/_paypal/webhook/', json.dumps(
+ {
+ "create_time": "2026-08-17T12:23:30.687Z",
+ "event_type": "PAYMENT.CAPTURE.DECLINED",
+ "event_version": "1.0",
+ "id": "WH-XXXXXXXXXXXX-XXXXXXXXX",
+ "links": [
+ {
+ "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-XXXXXXXXXXXX-XXXXXXXXX",
+ "method": "GET",
+ "rel": "self"
+ },
+ {
+ "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-XXXXXXXXXXXX-XXXXXXXXX/resend",
+ "method": "POST",
+ "rel": "resend"
+ }
+ ],
+ "resource": {
+ "amount": {},
+ "custom_id": "Order ABC-12345",
+ "disbursement_mode": "INSTANT",
+ "final_capture": True,
+ "id": "806440346Y391300T",
+ "payee": {},
+ "seller_protection": {},
+ "seller_receivable_breakdown": {},
+ "status": "DECLINED",
+ "supplementary_data": {}
+ },
+ "resource_type": "capture",
+ "resource_version": "2.0",
+ "summary": "A payment capture for € 30.0 EUR was declined."
+ }), content_type='application_json')
+
+ order = env[1]
+ order.refresh_from_db()
+ with scopes_disabled():
+ assert order.payments.first().state == OrderPayment.PAYMENT_STATE_FAILED
diff --git a/src/tests/plugins/sendmail/conftest.py b/src/tests/plugins/sendmail/conftest.py
index 3081c975b..3712dc7e2 100644
--- a/src/tests/plugins/sendmail/conftest.py
+++ b/src/tests/plugins/sendmail/conftest.py
@@ -35,6 +35,8 @@ def event():
organizer=o, name='Dummy', slug='dummy',
date_from=now(), live=True,
plugins='pretix.plugins.sendmail,tests.testdummy',
+ location='Foo City',
+ date_admission=now().replace(hour=11, minute=30),
)
return event
@@ -81,7 +83,12 @@ def event_series(event):
@pytest.fixture
def subevent1(event_series):
- se1 = event_series.subevents.create(name='Meow', date_from=now() + datetime.timedelta(days=1))
+ se1 = event_series.subevents.create(
+ name='Meow',
+ date_from=now() + datetime.timedelta(days=1),
+ location='Meow Town',
+ date_admission=now().replace(hour=10, minute=0),
+ )
return se1
diff --git a/src/tests/plugins/sendmail/test_rules.py b/src/tests/plugins/sendmail/test_rules.py
index 41147c6ae..290d066f9 100644
--- a/src/tests/plugins/sendmail/test_rules.py
+++ b/src/tests/plugins/sendmail/test_rules.py
@@ -186,7 +186,7 @@ def test_sendmail_rule_send_order_vs_pos(send_to, amount_mails, recipients, orde
order.event.sendmail_rules.create(date_is_absolute=True, send_date=dt_now - datetime.timedelta(hours=1),
send_to=send_to,
- subject='meow', template='meow meow meow')
+ subject='{event}: {event_location} @ {event_admission_time}', template='meow meow meow')
order.all_positions.create(item=item, price=0, attendee_email='meow@dummy.test')
sendmail_run_rules(None)
@@ -196,6 +196,8 @@ def test_sendmail_rule_send_order_vs_pos(send_to, amount_mails, recipients, orde
_recipients = [mail.to[0] for mail in djmail.outbox]
assert set(recipients) == set(_recipients)
+ assert djmail.outbox[0].subject == 'Dummy: Foo City @ 11:30'
+
@pytest.mark.django_db
@scopes_disabled()
@@ -243,7 +245,7 @@ def test_sendmail_rule_send_correct_subevent(order, event_series, subevent1, sub
event_series.sendmail_rules.create(date_is_absolute=False, offset_is_after=False, send_offset_days=2,
send_offset_time=datetime.time(9, 30), send_to=Rule.ATTENDEES,
- subject='meow', template='meow meow meow')
+ subject='{event}: {event_location} @ {event_admission_time}', template='meow meow meow')
p1 = order.all_positions.create(item=item, price=13, attendee_email='se1@dummy.test', subevent=subevent1)
order.all_positions.create(item=item, price=23, attendee_email='se2@dummy.test', subevent=subevent2)
@@ -253,6 +255,8 @@ def test_sendmail_rule_send_correct_subevent(order, event_series, subevent1, sub
assert djmail.outbox[0].to[0] == p1.attendee_email
+ assert djmail.outbox[0].subject == 'Meow: Meow Town @ 10:00'
+
@pytest.mark.django_db
@scopes_disabled()
diff --git a/src/tests/presale/test_cart.py b/src/tests/presale/test_cart.py
index ab0a852fc..72eb9661f 100644
--- a/src/tests/presale/test_cart.py
+++ b/src/tests/presale/test_cart.py
@@ -697,6 +697,42 @@ class CartTest(CartTestMixin, TestCase):
self.assertIsNone(objs[0].variation)
self.assertEqual(objs[0].price, 23)
+ def test_free_price_rounding(self):
+ self.ticket.free_price = True
+ self.ticket.save()
+
+ response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
+ 'item_%d' % self.ticket.id: '1',
+ 'price_%d' % self.ticket.id: '40.1234',
+ }, follow=True)
+ self.assertRedirects(response, '/%s/%s/?require_cookie=true' % (self.orga.slug, self.event.slug),
+ target_status_code=200)
+
+ with scopes_disabled():
+ cr1 = CartPosition.objects.get()
+ assert cr1.listed_price == Decimal('23.00')
+ assert cr1.custom_price_input == Decimal('40.12')
+ assert cr1.price == Decimal('40.12')
+
+ def test_free_price_rounding_jpy(self):
+ self.event.currency = "JPY"
+ self.event.save()
+ self.ticket.free_price = True
+ self.ticket.save()
+
+ response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
+ 'item_%d' % self.ticket.id: '1',
+ 'price_%d' % self.ticket.id: '40.1234',
+ }, follow=True)
+ self.assertRedirects(response, '/%s/%s/?require_cookie=true' % (self.orga.slug, self.event.slug),
+ target_status_code=200)
+
+ with scopes_disabled():
+ cr1 = CartPosition.objects.get()
+ assert cr1.listed_price == Decimal('23.00')
+ assert cr1.custom_price_input == Decimal('40.00')
+ assert cr1.price == Decimal('40.00')
+
def test_variation_inactive(self):
self.shirt_red.active = False
self.shirt_red.save()
@@ -3060,6 +3096,31 @@ class CartAddonTest(CartTestMixin, TestCase):
assert cp1.addons.count() == 3
assert all(a.price == Decimal('12.00') for a in cp1.addons.all())
+ @classscope(attr='orga')
+ def test_free_price_rounding(self):
+ self.event.settings.locales = ['de']
+ self.event.settings.locale = 'de'
+ self.event.currency = "JPY"
+ self.event.save()
+
+ self.workshop1.free_price = True
+ self.workshop1.save()
+ cp1 = CartPosition.objects.create(
+ event=self.event, cart_id=self.session_key, item=self.ticket,
+ price=23, expires=now() - timedelta(minutes=10)
+ )
+
+ response = self.client.post('/%s/%s/checkout/addons/' % (self.orga.slug, self.event.slug), {
+ 'cp_{}_item_{}'.format(cp1.pk, self.workshop1.pk): '1',
+ 'cp_{}_item_{}_price'.format(cp1.pk, self.workshop1.pk): '99,99',
+ }, follow=True)
+ self.assertRedirects(response, '/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug),
+ target_status_code=200)
+ with scopes_disabled():
+ assert cp1.addons.count() == 1
+ assert cp1.addons.first().item == self.workshop1
+ assert cp1.addons.first().price == Decimal('100')
+
@classscope(attr='orga')
def test_change_number(self):
cp1 = CartPosition.objects.create(
diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py
index 15f99f030..5a92cd746 100644
--- a/src/tests/presale/test_checkout.py
+++ b/src/tests/presale/test_checkout.py
@@ -478,6 +478,58 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
assert ia.vat_id == "AT123456"
assert not ia.vat_id_validated
+ def test_vat_id_revalidated_after_country_change(self):
+ self.tr19.eu_reverse_charge = True
+ self.tr19.keep_gross_if_rate_changes = True
+ self.tr19.home_country = Country('DE')
+ self.tr19.save()
+ self.event.settings.invoice_address_vatid = True
+
+ with scopes_disabled():
+ cr1 = CartPosition.objects.create(
+ event=self.event, cart_id=self.session_key, item=self.ticket,
+ price=23, expires=now() + timedelta(minutes=10)
+ )
+
+ with mock.patch('pretix.base.services.tax._validate_vat_id_EU') as mock_validate:
+ mock_validate.return_value = 'AT123456'
+ self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
+ 'is_business': 'business',
+ 'company': 'Foo',
+ 'name': 'Bar',
+ 'street': 'Baz',
+ 'zipcode': '1234',
+ 'city': 'Here',
+ 'country': 'AT',
+ 'vat_id': 'AT123456',
+ 'email': 'admin@localhost',
+ 'transmission_type': 'email',
+ }, follow=True)
+
+ cr1.refresh_from_db()
+ assert cr1.price == Decimal('23.00')
+ assert cr1.tax_rate == Decimal('0.00')
+ assert cr1.tax_value == Decimal('0.00')
+
+ with mock.patch('pretix.base.services.tax._validate_vat_id_EU') as mock_validate:
+ def raiser(*args, **kwargs):
+ raise VATIDFinalError('country mismatch')
+
+ mock_validate.side_effect = raiser
+ r = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
+ 'is_business': 'business',
+ 'company': 'Foo',
+ 'name': 'Bar',
+ 'street': 'Baz',
+ 'zipcode': '1012 WX',
+ 'city': 'Here',
+ 'country': 'NL',
+ 'vat_id': 'AT123456',
+ 'email': 'admin@localhost',
+ 'transmission_type': 'email',
+ }, follow=True)
+ assert b'alert-danger' in r.content
+
def test_reverse_charge_keep_gross(self):
self.tr19.eu_reverse_charge = True
self.tr19.keep_gross_if_rate_changes = True
@@ -2372,6 +2424,97 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
assert p2.fee.value == Decimal("0.46")
assert o.total == Decimal("25.76")
+ def test_payment_postpone_not_allowed(self):
+ self.event.settings.set('payment_banktransfer__enabled', True)
+ with scopes_disabled():
+ CartPosition.objects.create(
+ event=self.event, cart_id=self.session_key, item=self.ticket,
+ price=23, expires=now() + timedelta(minutes=10)
+ )
+ response = self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), {
+ 'postpone': 'on',
+ }, follow=False)
+ assert 'Please select' in response.content.decode()
+
+ def test_payment_postpone_allowed(self):
+ self.event.settings.set('payment_banktransfer__enabled', True)
+ self.event.settings.payment_choice_postpone_allowed_channels = ['web']
+ with scopes_disabled():
+ CartPosition.objects.create(
+ event=self.event, cart_id=self.session_key, item=self.ticket,
+ price=23, expires=now() + timedelta(minutes=10)
+ )
+ response = self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), {
+ 'postpone': 'on',
+ }, follow=True)
+ self.assertRedirects(response, '/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug),
+ target_status_code=200)
+
+ response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
+ doc = BeautifulSoup(response.content.decode(), "lxml")
+ self.assertEqual(len(doc.select(".thank-you")), 1)
+ with scopes_disabled():
+ o = Order.objects.last()
+ assert not o.payments.exists()
+
+ def test_payment_postpone_cleared_on_selection(self):
+ self.event.settings.set('payment_banktransfer__enabled', True)
+ self.event.settings.payment_choice_postpone_allowed_channels = ['web']
+ with scopes_disabled():
+ CartPosition.objects.create(
+ event=self.event, cart_id=self.session_key, item=self.ticket,
+ price=23, expires=now() + timedelta(minutes=10)
+ )
+
+ response = self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), {
+ 'postpone': 'on',
+ }, follow=False)
+ self.assertRedirects(response, '/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug),
+ target_status_code=200)
+ assert self.client.session['carts'][self.session_key].get('payments_postpone')
+
+ # The only available provider must not be preselected while the choice is postponed
+ response = self.client.get('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), follow=True)
+ doc = BeautifulSoup(response.content.decode(), "lxml")
+ self.assertEqual(len(doc.select('input[name="payment"]')), 1)
+ self.assertEqual(len(doc.select('input[name="payment"][checked]')), 0)
+
+ # Selecting a payment method takes the order out of the postponed state again
+ response = self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), {
+ 'payment': 'banktransfer',
+ }, follow=False)
+ self.assertRedirects(response, '/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug),
+ target_status_code=200)
+ assert not self.client.session['carts'][self.session_key].get('payments_postpone')
+
+ def test_payment_postpone_disabled_with_partial_payment(self):
+ self.event.settings.set('payment_banktransfer__enabled', True)
+ self.event.settings.payment_choice_postpone_allowed_channels = ['web']
+ gc = self.orga.issued_gift_cards.create(currency="EUR")
+ gc.transactions.create(value=20, acceptor=self.orga)
+ with scopes_disabled():
+ CartPosition.objects.create(
+ event=self.event, cart_id=self.session_key, item=self.ticket,
+ price=23, expires=now() + timedelta(minutes=10)
+ )
+
+ response = self.client.get('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), follow=True)
+ doc = BeautifulSoup(response.content.decode(), "lxml")
+ self.assertEqual(len(doc.select('button[name="postpone"]')), 1)
+ self.assertEqual(len(doc.select('button[name="postpone"][disabled]')), 0)
+
+ # Apply a gift card that only covers part of the total
+ response = self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), {
+ 'payment': 'giftcard',
+ 'payment_giftcard-code': gc.secret,
+ }, follow=True)
+ self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
+ target_status_code=200)
+
+ # Postponing would silently drop the gift card, so it is no longer offered
+ doc = BeautifulSoup(response.content.decode(), "lxml")
+ self.assertEqual(len(doc.select('button[name="postpone"][disabled]')), 1)
+
def test_premature_confirm(self):
response = self.client.get('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
self.assertRedirects(response, '/%s/%s/?require_cookie=true' % (self.orga.slug, self.event.slug),
@@ -5959,3 +6102,14 @@ class CustomerCheckoutTestCase(BaseCheckoutTestCase, TestCase):
response = self.client.get('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), follow=True)
assert 'Gift card' in response.content.decode()
assert '(1 available)' in response.content.decode()
+
+ def test_validation_bypass_error_async_id_is_fixed(self):
+ with scopes_disabled():
+ CartPosition.objects.create(
+ event=self.event, cart_id=self.session_key, item=self.ticket,
+ price=0, listed_price=0, price_after_voucher=0, expires=now() + timedelta(minutes=10)
+ )
+
+ response = self.client.post('/%s/%s/checkout/confirm/?async_id=1' % (self.orga.slug, self.event.slug), follow=False)
+ self.assertRedirects(response, '/%s/%s/checkout/customer/' % (self.orga.slug, self.event.slug),
+ target_status_code=200)
diff --git a/src/tests/presale/test_customer.py b/src/tests/presale/test_customer.py
index dee2eabb4..506b1f480 100644
--- a/src/tests/presale/test_customer.py
+++ b/src/tests/presale/test_customer.py
@@ -721,9 +721,21 @@ def _cross_domain_login(env, client, client2, org_alt=False):
else:
KnownDomain.objects.create(domainname='event.test', organizer=env[0], event=env[1])
- # Log in on org domain
+ # Start session on event domain
path = '/conf/' if org_alt else '/'
- r = client.post(f'/account/login?next=https://event.test{path}redeem&request_cross_domain_customer_auth=true', {
+ r = client2.post(f'{path}account/loginstart', {
+ 'next': f'https://event.test{path}redeem',
+ }, HTTP_HOST='event.test')
+ assert r.status_code == 302
+ u = urlparse(r.headers['Location'])
+ assert u.netloc == 'org.test'
+ assert u.path == '/account/login'
+ assert 'request_cross_domain_customer_auth=' in u.query
+ assert 'request_cross_domain_customer_auth_nonce=' in u.query
+ assert 'next=' in u.query
+
+ # Log in on org domain
+ r = client.post(f'{u.path}?{u.query}', {
'email': 'john@example.org',
'password': 'foo',
}, HTTP_HOST='org.test')
@@ -734,6 +746,7 @@ def _cross_domain_login(env, client, client2, org_alt=False):
assert u.path == path + 'redeem'
q = parse_qs(u.query)
assert 'cross_domain_customer_auth' in q
+ assert 'cross_domain_customer_auth_nonce' in q
# Take session over to event domain
r = client2.get(f'{path}?{u.query}', HTTP_HOST='event.test')
@@ -745,7 +758,7 @@ def _cross_domain_login(env, client, client2, org_alt=False):
def test_cross_domain_login(env, client, client2):
_cross_domain_login(env, client, client2)
- # Logged in on evnet domain
+ # Logged in on event domain
r = client.get('/', HTTP_HOST='event.test')
assert r.status_code == 200
assert b'john@example.org' in r.content
@@ -896,7 +909,14 @@ def test_cross_domain_login_with_sso(env, client, client2, provider):
},
)
- url = f'/account/login/{provider.pk}/?next=https://event.test/redeem&request_cross_domain_customer_auth=true'
+ r = client2.post('/account/loginstart', {"next": "https://event.test/redeem"}, follow=False, HTTP_HOST='event.test')
+ assert r.status_code == 302
+ assert "/account/login" in r['Location']
+
+ u = urlparse(r.headers['Location'])
+ nonce = parse_qs(u.query)['request_cross_domain_customer_auth_nonce'][0]
+ url = (f'/account/login/{provider.pk}/?next=https://event.test/redeem&request_cross_domain_customer_auth=true&'
+ f'request_cross_domain_customer_auth_nonce={nonce}')
r = client.get(url, follow=False, HTTP_HOST='org.test')
assert r.status_code == 302
assert "/authorize" in r['Location']
@@ -910,6 +930,7 @@ def test_cross_domain_login_with_sso(env, client, client2, provider):
assert u.path == '/redeem'
q = parse_qs(u.query)
assert 'cross_domain_customer_auth' in q
+ assert 'cross_domain_customer_auth_nonce' in q
# Take session over to event domain
r = client2.get(f'/?{u.query}', HTTP_HOST='event.test')
diff --git a/src/tests/presale/test_event.py b/src/tests/presale/test_event.py
index 70dade2be..d0bf3a3eb 100644
--- a/src/tests/presale/test_event.py
+++ b/src/tests/presale/test_event.py
@@ -744,6 +744,35 @@ class ItemDisplayTest(EventTestMixin, SoupTest):
self.assertNotIn("SOLD OUT", doc.select("section:nth-of-type(1)")[0].text)
self.assertIn("Late-bird", doc.select("section:nth-of-type(1)")[0].text)
+ def test_hidden_if_item_available_variation_unavailable_by_time(self):
+ with scopes_disabled():
+ q = Quota.objects.create(event=self.event, name='Early-bird', size=10)
+ q2 = Quota.objects.create(event=self.event, name='Late-bird', size=10)
+ item_with_vars = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=12)
+ v = item_with_vars.variations.create(
+ value='Regular', active=True,
+ )
+ item2 = Item.objects.create(event=self.event, name='Late-bird ticket', default_price=12,
+ hidden_if_item_available=item_with_vars)
+ q.items.add(item_with_vars)
+ q.variations.add(v)
+ q2.items.add(item2)
+
+ self.event.settings.hide_sold_out = True
+ doc = self.get_doc('/%s/%s/' % (self.orga.slug, self.event.slug))
+ self.assertIn("Early-bird", doc.select("section:nth-of-type(1)")[0].text)
+ self.assertNotIn("SOLD OUT", doc.select("section:nth-of-type(1)")[0].text)
+ self.assertNotIn("Late-bird", doc.select("section:nth-of-type(1)")[0].text)
+
+ item_with_vars.available_until = now() - datetime.timedelta(days=3)
+ item_with_vars.available_until_mode = "hide"
+ item_with_vars.save()
+
+ doc = self.get_doc('/%s/%s/' % (self.orga.slug, self.event.slug))
+ self.assertNotIn("Early-bird", doc.select("section:nth-of-type(1)")[0].text)
+ self.assertNotIn("SOLD OUT", doc.select("section:nth-of-type(1)")[0].text)
+ self.assertIn("Late-bird", doc.select("section:nth-of-type(1)")[0].text)
+
def test_bundle_sold_out(self):
with scopes_disabled():
q = Quota.objects.create(event=self.event, name='Quota', size=2)
@@ -884,7 +913,7 @@ class VoucherRedeemItemDisplayTest(EventTestMixin, SoupTest):
self.q.size = 0
self.q.save()
html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code))
- assert "_voucher_item" not in html.rendered_content
+ assert "_selected_item" not in html.rendered_content
def test_sold_out_blocking(self):
self.q.size = 0
@@ -892,7 +921,7 @@ class VoucherRedeemItemDisplayTest(EventTestMixin, SoupTest):
self.v.block_quota = True
self.v.save()
html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code))
- assert "_voucher_item" in html.rendered_content
+ assert "_selected_item" in html.rendered_content
def test_sold_out_ignore(self):
self.q.size = 0
@@ -900,7 +929,7 @@ class VoucherRedeemItemDisplayTest(EventTestMixin, SoupTest):
self.v.allow_ignore_quota = True
self.v.save()
html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code))
- assert "_voucher_item" in html.rendered_content
+ assert "_selected_item" in html.rendered_content
def test_variations_sold_out(self):
with scopes_disabled():
@@ -911,7 +940,7 @@ class VoucherRedeemItemDisplayTest(EventTestMixin, SoupTest):
self.q.size = 0
self.q.save()
html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code))
- assert "_voucher_item" not in html.rendered_content
+ assert "_selected_item" not in html.rendered_content
def test_variations_sold_out_blocking(self):
with scopes_disabled():
@@ -924,7 +953,27 @@ class VoucherRedeemItemDisplayTest(EventTestMixin, SoupTest):
self.v.block_quota = True
self.v.save()
html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code))
- assert "_voucher_item" in html.rendered_content
+ assert "_selected_item" in html.rendered_content
+
+ def test_variation_not_available(self):
+ with scopes_disabled():
+ var1 = ItemVariation.objects.create(item=self.item, value='Red', position=1)
+ var2 = ItemVariation.objects.create(item=self.item, value='Black', position=2, available_from=now() + datetime.timedelta(days=2))
+ var3 = ItemVariation.objects.create(item=self.item, value='Purple', position=3,
+ available_from=now() + datetime.timedelta(days=2), available_from_mode=Item.UNAVAIL_MODE_INFO)
+ self.q.variations.add(var1)
+ self.q.variations.add(var2)
+ self.q.variations.add(var3)
+ self.q.size = 0
+ self.q.save()
+ self.v.block_quota = True
+ self.v.save()
+ html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, self.v.code))
+ assert "_selected_item" in html.rendered_content
+ assert "Black" not in html.rendered_content
+ assert "Red" in html.rendered_content
+ assert "Purple" in html.rendered_content
+ assert "Not available yet" in html.rendered_content
def test_voucher_price(self):
self.v.value = Decimal("10.00")
@@ -1132,7 +1181,7 @@ class WaitingListTest(EventTestMixin, SoupTest):
def test_auto_disable(self):
self.event.settings.set('waiting_list_enabled', True)
self.event.settings.waiting_list_auto_disable = RelativeDateWrapper(
- RelativeDate(days=900, time=datetime.time(9, 0, 0), base_date_name='date_from', minutes=None, is_after=False)
+ RelativeDate(days=900, time=datetime.time(9, 0, 0), base_date_name='event__date_from', minutes=None, is_after=False)
)
response = self.client.get(
'/%s/%s/' % (self.orga.slug, self.event.slug)
diff --git a/src/tests/presale/test_order_change.py b/src/tests/presale/test_order_change.py
index b53d6ce59..648262578 100644
--- a/src/tests/presale/test_order_change.py
+++ b/src/tests/presale/test_order_change.py
@@ -1527,7 +1527,7 @@ class OrderChangeAddonsTest(BaseOrdersTest):
self.order.save()
self._subevent_setup()
self.event.settings.set('payment_term_last', RelativeDateWrapper(
- RelativeDate(days=2, time=None, base_date_name='date_from', minutes=None)
+ RelativeDate(days=2, time=None, base_date_name='event__date_from', minutes=None)
))
response = self.client.get(
diff --git a/src/tests/presale/test_orders.py b/src/tests/presale/test_orders.py
index 9606938b8..e67d7674d 100644
--- a/src/tests/presale/test_orders.py
+++ b/src/tests/presale/test_orders.py
@@ -37,18 +37,20 @@ import re
from decimal import Decimal
from bs4 import BeautifulSoup
+from django.core import mail as djmail
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase
from django.utils.timezone import now
from django_scopes import scopes_disabled
from pretix.base.models import (
- Event, Item, ItemCategory, ItemVariation, Order, OrderPosition, Organizer,
- Question, Quota,
+ Event, Invoice, InvoiceAddress, Item, ItemCategory, ItemVariation, Order,
+ OrderPosition, Organizer, Question, Quota,
)
from pretix.base.models.orders import OrderFee, OrderPayment
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
from pretix.base.services.invoices import generate_invoice
+from pretix.testutils.scope import classscope
class BaseOrdersTest(TestCase):
@@ -1076,7 +1078,7 @@ class OrdersTest(BaseOrdersTest):
self.event.date_from = now() + datetime.timedelta(days=3)
self.event.save()
self.event.settings.set('ticket_download_date', RelativeDateWrapper(RelativeDate(
- base_date_name='date_from', days=2, time=None, minutes=None
+ base_date_name='event__date_from', days=2, time=None, minutes=None
)))
response = self.client.post(
'/%s/%s/order/%s/%s/download/%d/testdummy' % (self.orga.slug, self.event.slug, self.order.code,
@@ -1702,6 +1704,63 @@ class OrdersTest(BaseOrdersTest):
assert 'Gift card' in response.content.decode()
assert '1 available' in response.content.decode()
+ @classscope("orga")
+ def test_change_paymentmethod_invoice(self):
+ self.event.settings.payment_banktransfer__enabled = True
+ self.event.settings.payment_banktransfer_invoice_immediately = True
+ self.event.settings.invoice_generate = "paid"
+ InvoiceAddress.objects.create(
+ order=self.order,
+ transmission_type="email"
+ )
+
+ assert self.order.invoices.count() == 0
+
+ with self.captureOnCommitCallbacks(execute=True):
+ self.client.post(
+ '/%s/%s/order/%s/%s/pay/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
+ {
+ 'payment': 'banktransfer'
+ }
+ )
+
+ assert self.order.payments.last().provider == 'banktransfer'
+ assert self.order.invoices.count() == 1
+ i = self.order.invoices.last()
+ assert i.transmission_status == Invoice.TRANSMISSION_STATUS_PENDING
+ assert len(djmail.outbox) == 0
+
+ @classscope("orga")
+ def test_change_paymentmethod_invoice_separately(self):
+ self.event.settings.payment_banktransfer__enabled = True
+ self.event.settings.payment_banktransfer_invoice_immediately = True
+ self.event.settings.invoice_generate = "paid"
+ InvoiceAddress.objects.create(
+ order=self.order,
+ transmission_type="email",
+ transmission_info={
+ "transmission_email_other": True,
+ "transmission_email_address": "invoice@example.org",
+ }
+ )
+
+ assert self.order.invoices.count() == 0
+
+ with self.captureOnCommitCallbacks(execute=True):
+ self.client.post(
+ '/%s/%s/order/%s/%s/pay/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
+ {
+ 'payment': 'banktransfer'
+ }
+ )
+
+ assert self.order.payments.last().provider == 'banktransfer'
+ assert self.order.invoices.count() == 1
+ i = self.order.invoices.last()
+ assert i.transmission_status == Invoice.TRANSMISSION_STATUS_COMPLETED
+ assert ["invoice@example.org"] == djmail.outbox[0].to
+ assert any(["Invoice_" in a[0] for a in djmail.outbox[0].attachments])
+
def test_answer_download_token(self):
with scopes_disabled():
q = self.event.questions.create(question="Foo", type="F")
diff --git a/src/tests/presale/test_widget.py b/src/tests/presale/test_widget.py
index 10795cb15..0738326d7 100644
--- a/src/tests/presale/test_widget.py
+++ b/src/tests/presale/test_widget.py
@@ -173,6 +173,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"frontpage_text": "",
"location": "",
"currency": "EUR",
+ "currency_places": 2,
"show_variations_expanded": False,
"display_net_prices": False,
"use_native_spinners": False,
@@ -379,6 +380,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"frontpage_text": "",
"location": "",
"currency": "EUR",
+ "currency_places": 2,
"show_variations_expanded": False,
"display_net_prices": False,
"use_native_spinners": False,
@@ -439,6 +441,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"frontpage_text": "",
"location": "",
"currency": "EUR",
+ "currency_places": 2,
"show_variations_expanded": False,
"display_net_prices": False,
"use_native_spinners": False,
@@ -524,6 +527,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
"frontpage_text": "",
"location": "",
"currency": "EUR",
+ "currency_places": 2,
'poweredby': 'ticketing powered by pretix',
"show_variations_expanded": False,
"display_net_prices": False,
diff --git a/src/tests/testdummy/urls.py b/src/tests/testdummy/urls.py
index c4abab349..78d59cc28 100644
--- a/src/tests/testdummy/urls.py
+++ b/src/tests/testdummy/urls.py
@@ -21,6 +21,7 @@
#
from django.http import HttpResponse
from django.urls import path
+from django.views.decorators.csrf import ensure_csrf_cookie
def view(request):
@@ -33,6 +34,7 @@ urlpatterns = [
view,
name="view",
),
+ path('ensure_csrf_cookie/', ensure_csrf_cookie(view), name="ensure_csrf_cookie"),
]
organizer_patterns = [
diff --git a/vite.config.ts b/vite.config.ts
index b0578d76c..1bcace40a 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -31,6 +31,9 @@ export default defineConfig({
// Allow serving source files from sibling plugin directories
allow: ['src', ...pluginDirs],
},
+ cors: {
+ origin: /^https?:\/\/(?:(?:[^:]+\.)?localhost|127\.0\.0\.1|\[::1\]|[^:]+\.pretix\.(dev|work))(?::\d+)?$/
+ },
},
build: {
manifest: true,