mirror of
https://github.com/pretix/pretix.git
synced 2026-08-03 09:37:51 +00:00
Compare commits
145
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8164f469d3 | ||
|
|
b5ad372bb2 | ||
|
|
cdea82d206 | ||
|
|
de9045afcf | ||
|
|
6b65cb4e33 | ||
|
|
c4792800f0 | ||
|
|
ca23f7ebc2 | ||
|
|
0259899e00 | ||
|
|
efb94265b2 | ||
|
|
2aa27f56f1 | ||
|
|
4f3d90fc50 | ||
|
|
9cf66de437 | ||
|
|
9f4cbabd30 | ||
|
|
0fc2d6134f | ||
|
|
1e0e16642d | ||
|
|
a58403559e | ||
|
|
dfd53f0ea2 | ||
|
|
06250ef55e | ||
|
|
ab3104fe65 | ||
|
|
bb6e424cde | ||
|
|
c2623dba60 | ||
|
|
d8f7465b03 | ||
|
|
ac0546499b | ||
|
|
ebbb532478 | ||
|
|
94dad4d0d2 | ||
|
|
a06cd687ba | ||
|
|
fd9f3ea6ed | ||
|
|
608622e3f3 | ||
|
|
4d94294e5a | ||
|
|
4dbdadabb5 | ||
|
|
d494c61cba | ||
|
|
55a7dfbff3 | ||
|
|
b8c271cf9c | ||
|
|
5af7e1b6d6 | ||
|
|
9222ce0ecd | ||
|
|
8afb0e43e0 | ||
|
|
c65fecf45e | ||
|
|
1c684d62d4 | ||
|
|
48809dc477 | ||
|
|
71df116079 | ||
|
|
ad64f6e88b | ||
|
|
891ba9d99c | ||
|
|
5cd1476a07 | ||
|
|
cb393a0b31 | ||
|
|
af59a89ecb | ||
|
|
1eb0008da9 | ||
|
|
d6489c6dd8 | ||
|
|
abe6acc9d8 | ||
|
|
2dcbb791f0 | ||
|
|
2efc40e20b | ||
|
|
0693681473 | ||
|
|
3aabc8a163 | ||
|
|
062f8fa409 | ||
|
|
106339c928 | ||
|
|
222ea08dd0 | ||
|
|
62bc16f963 | ||
|
|
3332fc818a | ||
|
|
d87dbaf9e5 | ||
|
|
67580c4ca5 | ||
|
|
c5b32484b1 | ||
|
|
b5560509ad | ||
|
|
c78365ce43 | ||
|
|
8cc12fa1c7 | ||
|
|
59c09e27fd | ||
|
|
4d68d24eca | ||
|
|
cc5693017e | ||
|
|
6a07b7d5d1 | ||
|
|
26dc3486a0 | ||
|
|
de60183456 | ||
|
|
520bb9e378 | ||
|
|
97e344e81a | ||
|
|
a3f5f33ed5 | ||
|
|
5a123bf88f | ||
|
|
64c52a5e36 | ||
|
|
a60341afe9 | ||
|
|
308e14bab3 | ||
|
|
aa5f635932 | ||
|
|
66a9902eb4 | ||
|
|
79a58fe104 | ||
|
|
bb5a9bdbf1 | ||
|
|
449b960438 | ||
|
|
a3f247117c | ||
|
|
e279ecb423 | ||
|
|
ca6a650398 | ||
|
|
696e5602ac | ||
|
|
4c7987cef6 | ||
|
|
37c65030f8 | ||
|
|
0d1673136f | ||
|
|
32d8dce6aa | ||
|
|
8a2ecb4e97 | ||
|
|
91348e3b00 | ||
|
|
459f4f84c7 | ||
|
|
31a1385946 | ||
|
|
adfd0bfcfd | ||
|
|
ef7433dbcd | ||
|
|
ebbd18bb26 | ||
|
|
fc4ce102b6 | ||
|
|
8854ae3187 | ||
|
|
c5a91ef479 | ||
|
|
aa9c478c30 | ||
|
|
847dc0f992 | ||
|
|
daaae85865 | ||
|
|
06770bcef5 | ||
|
|
dc6eae4708 | ||
|
|
bf8bb78d2a | ||
|
|
091be266fc | ||
|
|
dde655f7d6 | ||
|
|
409e64d5f2 | ||
|
|
5d67a4fa33 | ||
|
|
4eb2c50d95 | ||
|
|
a7e85a157d | ||
|
|
4c3584c788 | ||
|
|
e466c4fb72 | ||
|
|
d0d7670ca5 | ||
|
|
a17a098b15 | ||
|
|
40516ab8e0 | ||
|
|
3ca343fabc | ||
|
|
7304b7f24b | ||
|
|
abaf968103 | ||
|
|
86e2f5a155 | ||
|
|
4c64af02c1 | ||
|
|
11df4398e1 | ||
|
|
2e89fc0a94 | ||
|
|
510c4850a5 | ||
|
|
b13368d614 | ||
|
|
b5cc8b368b | ||
|
|
87c30d0acb | ||
|
|
ffed8b29b1 | ||
|
|
53fbb64225 | ||
|
|
e10ec4074b | ||
|
|
7f2dc77aca | ||
|
|
199a3bf1e7 | ||
|
|
904aa807a3 | ||
|
|
0e41353a0e | ||
|
|
b895d9bbca | ||
|
|
f214edaf34 | ||
|
|
165a47b593 | ||
|
|
e06f281f1e | ||
|
|
203c7e660d | ||
|
|
8c360b8754 | ||
|
|
90b6511d11 | ||
|
|
bb356257cb | ||
|
|
e1950e408e | ||
|
|
99d5722ce1 | ||
|
|
324eeb8d40 |
@@ -26,10 +26,10 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.13
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.13
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
|
|||||||
@@ -23,13 +23,13 @@ jobs:
|
|||||||
name: Tests
|
name: Tests
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.9", "3.10", "3.11"]
|
python-version: ["3.10", "3.11", "3.13"]
|
||||||
database: [sqlite, postgres]
|
database: [sqlite, postgres]
|
||||||
exclude:
|
exclude:
|
||||||
- database: sqlite
|
|
||||||
python-version: "3.9"
|
|
||||||
- database: sqlite
|
- database: sqlite
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
|
- database: sqlite
|
||||||
|
python-version: "3.11"
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
|
|||||||
Vendored
+79
-95
@@ -6,10 +6,14 @@
|
|||||||
{%- else %}
|
{%- else %}
|
||||||
{%- set titlesuffix = "" %}
|
{%- set titlesuffix = "" %}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
{%- set lang_attr = 'en' if language == None else (language | replace('_', '-')) %}
|
||||||
|
|
||||||
|
{# Build sphinx_version_info tuple from sphinx_version string in pure Jinja #}
|
||||||
|
{%- set (_ver_major, _ver_minor) = (sphinx_version.split('.') | list)[:2] | map('int') -%}
|
||||||
|
{%- set sphinx_version_info = (_ver_major, _ver_minor, -1) -%}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
|
<html class="writer-html5" lang="{{ lang_attr }}"{% if sphinx_version_info >= (7, 2) %} data-content_root="{{ content_root }}"{% endif %}>
|
||||||
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
{{ metatags }}
|
{{ metatags }}
|
||||||
@@ -18,59 +22,50 @@
|
|||||||
<title>{{ title|striptags|e }}{{ titlesuffix }}</title>
|
<title>{{ title|striptags|e }}{{ titlesuffix }}</title>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{#- CSS #}
|
||||||
{#- CSS #}
|
{%- for css_file in css_files %}
|
||||||
{%- for css in css_files %}
|
{%- if css_file|attr("filename") %}
|
||||||
{%- if css|attr("rel") %}
|
{{ css_tag(css_file) }}
|
||||||
<link rel="{{ css.rel }}" href="{{ pathto(css.filename, 1) }}" type="text/css"{% if css.title is not none %} title="{{ css.title }}"{% endif %} />
|
|
||||||
{%- else %}
|
{%- else %}
|
||||||
<link rel="stylesheet" href="{{ pathto(css, 1) }}" type="text/css" />
|
<link rel="stylesheet" href="{{ pathto(css_file, 1)|escape }}" type="text/css" />
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
|
||||||
{%- for cssfile in extra_css_files %}
|
{#- FAVICON #}
|
||||||
<link rel="stylesheet" href="{{ pathto(cssfile, 1) }}" type="text/css" />
|
{%- if favicon_url %}
|
||||||
{%- endfor -%}
|
<link rel="shortcut icon" href="{{ favicon_url }}"/>
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
{#- FAVICON
|
{#- CANONICAL URL (deprecated) #}
|
||||||
favicon_url is the only context var necessary since Sphinx 4.
|
{%- if theme_canonical_url and not pageurl %}
|
||||||
In Sphinx<4, we use favicon but need to prepend path info.
|
|
||||||
#}
|
|
||||||
{%- set _favicon_url = favicon_url | default(pathto('_static/' + (favicon or ""), 1)) %}
|
|
||||||
{%- if favicon_url or favicon %}
|
|
||||||
<link rel="shortcut icon" href="{{ _favicon_url }}"/>
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
{#- CANONICAL URL (deprecated) #}
|
|
||||||
{%- if theme_canonical_url and not pageurl %}
|
|
||||||
<link rel="canonical" href="{{ theme_canonical_url }}{{ pagename }}.html"/>
|
<link rel="canonical" href="{{ theme_canonical_url }}{{ pagename }}.html"/>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
{#- CANONICAL URL #}
|
{#- CANONICAL URL #}
|
||||||
{%- if pageurl %}
|
{%- if pageurl %}
|
||||||
<link rel="canonical" href="{{ pageurl|e }}" />
|
<link rel="canonical" href="{{ pageurl|e }}" />
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
{#- JAVASCRIPTS #}
|
{#- JAVASCRIPTS #}
|
||||||
{%- block scripts %}
|
{%- block scripts %}
|
||||||
<!--[if lt IE 9]>
|
{%- if not embedded %}
|
||||||
<script src="{{ pathto('_static/js/html5shiv.min.js', 1) }}"></script>
|
{%- for scriptfile in script_files %}
|
||||||
<![endif]-->
|
{{ js_tag(scriptfile) }}
|
||||||
{%- if not embedded %}
|
{%- endfor %}
|
||||||
{# XXX Sphinx 1.8.0 made this an external js-file, quick fix until we refactor the template to inherert more blocks directly from sphinx #}
|
|
||||||
{%- for scriptfile in script_files %}
|
|
||||||
{{ js_tag(scriptfile) }}
|
|
||||||
{%- endfor %}
|
|
||||||
<script src="{{ pathto('_static/js/theme.js', 1) }}"></script>
|
<script src="{{ pathto('_static/js/theme.js', 1) }}"></script>
|
||||||
|
|
||||||
|
{%- if READTHEDOCS or DEBUG %}
|
||||||
|
<script src="{{ pathto('_static/js/versions.js', 1) }}"></script>
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
{#- OPENSEARCH #}
|
{#- OPENSEARCH #}
|
||||||
{%- if use_opensearch %}
|
{%- if use_opensearch %}
|
||||||
<link rel="search" type="application/opensearchdescription+xml"
|
<link rel="search" type="application/opensearchdescription+xml"
|
||||||
title="{% trans docstitle=docstitle|e %}Search within {{ docstitle }}{% endtrans %}"
|
title="{% trans docstitle=docstitle|e %}Search within {{ docstitle }}{% endtrans %}"
|
||||||
href="{{ pathto('_static/opensearch.xml', 1) }}"/>
|
href="{{ pathto('_static/opensearch.xml', 1) }}"/>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- endblock %}
|
{%- endblock %}
|
||||||
|
|
||||||
{%- block linktags %}
|
{%- block linktags %}
|
||||||
{%- if hasdoc('about') %}
|
{%- if hasdoc('about') %}
|
||||||
@@ -123,23 +118,23 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
|
{%- block navigation %}
|
||||||
{% block menu %}
|
{#- Translators: This is an ARIA section label for the main navigation menu -#}
|
||||||
{#
|
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="{{ _('Navigation menu') }}">
|
||||||
The singlehtml builder doesn't handle this toctree call when the
|
{%- block menu %}
|
||||||
toctree is empty. Skip building this for now.
|
{%- set toctree = toctree(maxdepth=theme_navigation_depth|int,
|
||||||
#}
|
collapse=theme_collapse_navigation|tobool,
|
||||||
{% if 'singlehtml' not in builder %}
|
includehidden=theme_includehidden|tobool,
|
||||||
{% set global_toc = toctree(maxdepth=theme_navigation_depth|int, collapse=theme_collapse_navigation, includehidden=True) %}
|
titles_only=theme_titles_only|tobool) %}
|
||||||
{% endif %}
|
{%- if toctree %}
|
||||||
{% if global_toc %}
|
{{ toctree }}
|
||||||
{{ global_toc }}
|
{%- else %}
|
||||||
{% else %}
|
|
||||||
<!-- Local TOC -->
|
<!-- Local TOC -->
|
||||||
<div class="local-toc">{{ toc }}</div>
|
<div class="local-toc">{{ toc }}</div>
|
||||||
{% endif %}
|
{%- endif %}
|
||||||
{% endblock %}
|
{%- endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
{%- endblock %}
|
||||||
|
|
||||||
{% if theme_display_version %}
|
{% if theme_display_version %}
|
||||||
{%- set nav_version = version %}
|
{%- set nav_version = version %}
|
||||||
@@ -158,53 +153,42 @@
|
|||||||
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
|
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
|
||||||
|
|
||||||
{# MOBILE NAV, TRIGGLES SIDE NAV ON TOGGLE #}
|
{# MOBILE NAV, TRIGGLES SIDE NAV ON TOGGLE #}
|
||||||
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
|
<nav class="wy-nav-top" aria-label="{{ _('Mobile navigation menu') }}" {% if theme_style_nav_header_background %} style="background: {{theme_style_nav_header_background}}" {% endif %}>
|
||||||
{% block mobile_nav %}
|
{%- block mobile_nav %}
|
||||||
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
|
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
|
||||||
<a href="{{ pathto('index') }}">{{ project }}</a>
|
<a href="{{ pathto(master_doc) }}">{{ project }}</a>
|
||||||
{% endblock %}
|
{%- endblock %}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<div class="wy-nav-content">
|
||||||
{# PAGE CONTENT #}
|
{%- block content %}
|
||||||
<div class="wy-nav-content">
|
{%- if theme_style_external_links|tobool %}
|
||||||
<div class="rst-content">
|
<div class="rst-content style-external-links">
|
||||||
{% include "breadcrumbs.html" %}
|
{%- else %}
|
||||||
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
|
<div class="rst-content">
|
||||||
<div itemprop="articleBody" class="section">
|
{%- endif %}
|
||||||
{% block body %}{% endblock %}
|
{% include "breadcrumbs.html" %}
|
||||||
</div>
|
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
|
||||||
<div class="articleComments">
|
{%- block document %}
|
||||||
{% block comments %}{% endblock %}
|
<div itemprop="articleBody">
|
||||||
</div>
|
{% block body %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
{% include "footer.html" %}
|
{%- if self.comments()|trim %}
|
||||||
|
<div class="articleComments">
|
||||||
|
{%- block comments %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
{%- endif%}
|
||||||
|
</div>
|
||||||
|
{%- endblock %}
|
||||||
|
{% include "footer.html" %}
|
||||||
|
</div>
|
||||||
|
{%- endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% include "versions.html" %}
|
{% include "versions.html" %}
|
||||||
|
|
||||||
{% if not embedded %}
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
var DOCUMENTATION_OPTIONS = {
|
|
||||||
URL_ROOT:'{{ url_root }}',
|
|
||||||
VERSION:'{{ release|e }}',
|
|
||||||
COLLAPSE_INDEX:false,
|
|
||||||
FILE_SUFFIX:'{{ '' if no_search_suffix else file_suffix }}',
|
|
||||||
HAS_SOURCE: {{ has_source|lower }},
|
|
||||||
SOURCELINK_SUFFIX: '{{ sourcelink_suffix }}'
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
{%- for scriptfile in script_files %}
|
|
||||||
<script type="text/javascript" src="{{ pathto(scriptfile, 1) }}"></script>
|
|
||||||
{%- endfor %}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{# RTD hosts this file, so just load on non RTD builds #}
|
{# RTD hosts this file, so just load on non RTD builds #}
|
||||||
{% if not READTHEDOCS %}
|
{% if not READTHEDOCS %}
|
||||||
<script type="text/javascript" src="{{ pathto('_static/js/theme.js', 1) }}"></script>
|
<script type="text/javascript" src="{{ pathto('_static/js/theme.js', 1) }}"></script>
|
||||||
@@ -214,7 +198,7 @@
|
|||||||
{% if theme_sticky_navigation %}
|
{% if theme_sticky_navigation %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
jQuery(function () {
|
jQuery(function () {
|
||||||
SphinxRtdTheme.StickyNav.enable();
|
SphinxRtdTheme.Navigation.enable({{ 'true' if theme_sticky_navigation|tobool else 'false' }});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
+184
-166
@@ -1,136 +1,86 @@
|
|||||||
{#
|
{# TEMPLATE VAR SETTINGS #}
|
||||||
basic/layout.html
|
|
||||||
~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Master layout template for Sphinx themes.
|
|
||||||
|
|
||||||
:copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS.
|
|
||||||
:license: BSD, see LICENSE for details.
|
|
||||||
#}
|
|
||||||
{%- block doctype -%}
|
|
||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
|
||||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
||||||
{%- endblock %}
|
|
||||||
{%- set reldelim1 = reldelim1 is not defined and ' »' or reldelim1 %}
|
|
||||||
{%- set reldelim2 = reldelim2 is not defined and ' |' or reldelim2 %}
|
|
||||||
{%- set render_sidebar = (not embedded) and (not theme_nosidebar|tobool) and
|
|
||||||
(sidebars != []) %}
|
|
||||||
{%- set url_root = pathto('', 1) %}
|
{%- set url_root = pathto('', 1) %}
|
||||||
{# XXX necessary? #}
|
|
||||||
{%- if url_root == '#' %}{% set url_root = '' %}{% endif %}
|
{%- if url_root == '#' %}{% set url_root = '' %}{% endif %}
|
||||||
{%- if not embedded and docstitle %}
|
{%- if not embedded and docstitle %}
|
||||||
{%- set titlesuffix = " — "|safe + docstitle|e %}
|
{%- set titlesuffix = " — "|safe + docstitle|e %}
|
||||||
{%- else %}
|
{%- else %}
|
||||||
{%- set titlesuffix = "" %}
|
{%- set titlesuffix = "" %}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
{%- set lang_attr = 'en' if language == None else (language | replace('_', '-')) %}
|
||||||
|
|
||||||
{%- macro relbar() %}
|
{# Build sphinx_version_info tuple from sphinx_version string in pure Jinja #}
|
||||||
<div class="related">
|
{%- set (_ver_major, _ver_minor) = (sphinx_version.split('.') | list)[:2] | map('int') -%}
|
||||||
<h3>{{ _('Navigation') }}</h3>
|
{%- set sphinx_version_info = (_ver_major, _ver_minor, -1) -%}
|
||||||
<ul>
|
|
||||||
{%- for rellink in rellinks %}
|
|
||||||
<li class="right" {% if loop.first %}style="margin-right: 10px"{% endif %}>
|
|
||||||
<a href="{{ pathto(rellink[0]) }}" title="{{ rellink[1]|striptags|e }}"
|
|
||||||
{{ accesskey(rellink[2]) }}>{{ rellink[3] }}</a>
|
|
||||||
{%- if not loop.first %}{{ reldelim2 }}{% endif %}</li>
|
|
||||||
{%- endfor %}
|
|
||||||
{%- block rootrellink %}
|
|
||||||
<li><a href="{{ pathto(master_doc) }}">{{ shorttitle|e }}</a>{{ reldelim1 }}</li>
|
|
||||||
{%- endblock %}
|
|
||||||
{%- for parent in parents %}
|
|
||||||
<li><a href="{{ parent.link|e }}" {% if loop.last %}{{ accesskey("U") }}{% endif %}>{{ parent.title }}</a>{{ reldelim1 }}</li>
|
|
||||||
{%- endfor %}
|
|
||||||
{%- block relbaritems %} {% endblock %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{%- endmacro %}
|
|
||||||
|
|
||||||
{%- macro sidebar() %}
|
<!DOCTYPE html>
|
||||||
{%- if render_sidebar %}
|
<html class="writer-html5" lang="{{ lang_attr }}"{% if sphinx_version_info >= (7, 2) %} data-content_root="{{ content_root }}"{% endif %}>
|
||||||
<div class="sphinxsidebar">
|
<head>
|
||||||
<div class="sphinxsidebarwrapper">
|
<meta charset="utf-8" />
|
||||||
{%- block sidebarlogo %}
|
{%- if READTHEDOCS and not embedded %}
|
||||||
{%- if logo %}
|
<meta name="readthedocs-addons-api-version" content="1">
|
||||||
<p class="logo"><a href="{{ pathto(master_doc) }}">
|
{%- endif %}
|
||||||
<img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/>
|
{{- metatags }}
|
||||||
</a></p>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
{%- endif %}
|
{%- block htmltitle %}
|
||||||
{%- endblock %}
|
<title>{{ title|striptags|e }}{{ titlesuffix }}</title>
|
||||||
{%- if sidebars != None %}
|
{%- endblock -%}
|
||||||
{#- new style sidebar: explicitly include/exclude templates #}
|
|
||||||
{%- for sidebartemplate in sidebars %}
|
|
||||||
{%- include sidebartemplate %}
|
|
||||||
{%- endfor %}
|
|
||||||
{%- else %}
|
|
||||||
{#- old style sidebars: using blocks -- should be deprecated #}
|
|
||||||
{%- block sidebartoc %}
|
|
||||||
{%- include "localtoc.html" %}
|
|
||||||
{%- endblock %}
|
|
||||||
{%- block sidebarrel %}
|
|
||||||
{%- include "relations.html" %}
|
|
||||||
{%- endblock %}
|
|
||||||
{%- block sidebarsourcelink %}
|
|
||||||
{%- include "sourcelink.html" %}
|
|
||||||
{%- endblock %}
|
|
||||||
{%- if customsidebar %}
|
|
||||||
{%- include customsidebar %}
|
|
||||||
{%- endif %}
|
|
||||||
{%- block sidebarsearch %}
|
|
||||||
{%- include "searchbox.html" %}
|
|
||||||
{%- endblock %}
|
|
||||||
{%- endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{%- endif %}
|
|
||||||
{%- endmacro %}
|
|
||||||
|
|
||||||
{%- macro script() %}
|
{#- CSS #}
|
||||||
<script type="text/javascript">
|
{%- for css_file in css_files %}
|
||||||
var DOCUMENTATION_OPTIONS = {
|
{%- if css_file|attr("filename") %}
|
||||||
URL_ROOT: '{{ url_root }}',
|
{{ css_tag(css_file) }}
|
||||||
VERSION: '{{ release|e }}',
|
{%- else %}
|
||||||
COLLAPSE_INDEX: false,
|
<link rel="stylesheet" href="{{ pathto(css_file, 1)|escape }}" type="text/css" />
|
||||||
FILE_SUFFIX: '{{ '' if no_search_suffix else file_suffix }}',
|
{%- endif %}
|
||||||
HAS_SOURCE: {{ has_source|lower }},
|
{%- endfor %}
|
||||||
SOURCELINK_SUFFIX: '{{ sourcelink_suffix }}'
|
|
||||||
};
|
{#
|
||||||
</script>
|
"extra_css_files" is an undocumented Read the Docs theme specific option.
|
||||||
|
There is no need to check for ``|attr("filename")`` here because it's always a string.
|
||||||
|
Note that this option should be removed in favor of regular ``html_css_files``:
|
||||||
|
https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_css_files
|
||||||
|
#}
|
||||||
|
{%- for css_file in extra_css_files %}
|
||||||
|
<link rel="stylesheet" href="{{ pathto(css_file, 1)|escape }}" type="text/css" />
|
||||||
|
{%- endfor -%}
|
||||||
|
|
||||||
|
{#- FAVICON #}
|
||||||
|
{%- if favicon_url %}
|
||||||
|
<link rel="shortcut icon" href="{{ favicon_url }}"/>
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
|
{#- CANONICAL URL (deprecated) #}
|
||||||
|
{%- if theme_canonical_url and not pageurl %}
|
||||||
|
<link rel="canonical" href="{{ theme_canonical_url }}{{ pagename }}.html"/>
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
|
{#- CANONICAL URL #}
|
||||||
|
{%- if pageurl %}
|
||||||
|
<link rel="canonical" href="{{ pageurl|e }}" />
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
|
{#- JAVASCRIPTS #}
|
||||||
|
{%- block scripts %}
|
||||||
|
{%- if not embedded %}
|
||||||
{%- for scriptfile in script_files %}
|
{%- for scriptfile in script_files %}
|
||||||
<script type="text/javascript" src="{{ pathto(scriptfile, 1) }}"></script>
|
{{ js_tag(scriptfile) }}
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
{%- endmacro %}
|
<script src="{{ pathto('_static/js/theme.js', 1) }}"></script>
|
||||||
|
|
||||||
{%- macro css() %}
|
{%- if READTHEDOCS or DEBUG %}
|
||||||
<link rel="stylesheet" href="{{ pathto('_static/' + style, 1) }}" type="text/css" />
|
<script src="{{ pathto('_static/js/versions.js', 1) }}"></script>
|
||||||
<link rel="stylesheet" href="{{ pathto('_static/pygments.css', 1) }}" type="text/css" />
|
{%- endif %}
|
||||||
{%- for cssfile in css_files %}
|
|
||||||
<link rel="stylesheet" href="{{ pathto(cssfile, 1) }}" type="text/css" />
|
|
||||||
{%- endfor %}
|
|
||||||
{%- endmacro %}
|
|
||||||
|
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
{#- OPENSEARCH #}
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset={{ encoding }}" />
|
|
||||||
{{ metatags }}
|
|
||||||
{%- block htmltitle %}
|
|
||||||
<title>{{ title|striptags|e }}{{ titlesuffix }}</title>
|
|
||||||
{%- endblock %}
|
|
||||||
{{ css() }}
|
|
||||||
{%- if not embedded %}
|
|
||||||
{{ script() }}
|
|
||||||
{%- if use_opensearch %}
|
{%- if use_opensearch %}
|
||||||
<link rel="search" type="application/opensearchdescription+xml"
|
<link rel="search" type="application/opensearchdescription+xml"
|
||||||
title="{% trans docstitle=docstitle|e %}Search within {{ docstitle }}{% endtrans %}"
|
title="{% trans docstitle=docstitle|e %}Search within {{ docstitle }}{% endtrans %}"
|
||||||
href="{{ pathto('_static/opensearch.xml', 1) }}"/>
|
href="{{ pathto('_static/opensearch.xml', 1) }}"/>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- if favicon %}
|
{%- endif %}
|
||||||
<link rel="shortcut icon" href="{{ pathto('_static/' + favicon, 1) }}"/>
|
{%- endblock %}
|
||||||
{%- endif %}
|
|
||||||
{%- if theme_canonical_url %}
|
{%- block linktags %}
|
||||||
<link rel="canonical" href="{{ theme_canonical_url }}{{ pagename }}.html"/>
|
|
||||||
{%- endif %}
|
|
||||||
{%- endif %}
|
|
||||||
{%- block linktags %}
|
|
||||||
{%- if hasdoc('about') %}
|
{%- if hasdoc('about') %}
|
||||||
<link rel="author" title="{{ _('About these documents') }}" href="{{ pathto('about') }}" />
|
<link rel="author" title="{{ _('About these documents') }}" href="{{ pathto('about') }}" />
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
@@ -143,67 +93,135 @@
|
|||||||
{%- if hasdoc('copyright') %}
|
{%- if hasdoc('copyright') %}
|
||||||
<link rel="copyright" title="{{ _('Copyright') }}" href="{{ pathto('copyright') }}" />
|
<link rel="copyright" title="{{ _('Copyright') }}" href="{{ pathto('copyright') }}" />
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
<link rel="top" title="{{ docstitle|e }}" href="{{ pathto('index') }}" />
|
|
||||||
{%- if parents %}
|
|
||||||
<link rel="up" title="{{ parents[-1].title|striptags|e }}" href="{{ parents[-1].link|e }}" />
|
|
||||||
{%- endif %}
|
|
||||||
{%- if next %}
|
{%- if next %}
|
||||||
<link rel="next" title="{{ next.title|striptags|e }}" href="{{ next.link|e }}" />
|
<link rel="next" title="{{ next.title|striptags|e }}" href="{{ next.link|e }}" />
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- if prev %}
|
{%- if prev %}
|
||||||
<link rel="prev" title="{{ prev.title|striptags|e }}" href="{{ prev.link|e }}" />
|
<link rel="prev" title="{{ prev.title|striptags|e }}" href="{{ prev.link|e }}" />
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- endblock %}
|
|
||||||
{%- block extrahead %} {% endblock %}
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{%- block header %}{% endblock %}
|
|
||||||
|
|
||||||
{%- block relbar1 %}{{ relbar() }}{% endblock %}
|
|
||||||
|
|
||||||
{%- block content %}
|
|
||||||
{%- block sidebar1 %} {# possible location for sidebar #} {% endblock %}
|
|
||||||
|
|
||||||
<div class="document">
|
|
||||||
{%- block document %}
|
|
||||||
<div class="documentwrapper">
|
|
||||||
{%- if render_sidebar %}
|
|
||||||
<div class="bodywrapper">
|
|
||||||
{%- endif %}
|
|
||||||
<div class="body">
|
|
||||||
{% block body %} {% endblock %}
|
|
||||||
</div>
|
|
||||||
{%- if render_sidebar %}
|
|
||||||
</div>
|
|
||||||
{%- endif %}
|
|
||||||
</div>
|
|
||||||
{%- endblock %}
|
{%- endblock %}
|
||||||
|
{%- block extrahead %} {% endblock %}
|
||||||
|
</head>
|
||||||
|
|
||||||
{%- block sidebar2 %}{{ sidebar() }}{% endblock %}
|
<body class="wy-body-for-nav">
|
||||||
<div class="clearer"></div>
|
|
||||||
</div>
|
|
||||||
{%- endblock %}
|
|
||||||
|
|
||||||
{%- block relbar2 %}{{ relbar() }}{% endblock %}
|
{%- block extrabody %} {% endblock %}
|
||||||
|
<div class="wy-grid-for-nav">
|
||||||
|
{#- SIDE NAV, TOGGLES ON MOBILE #}
|
||||||
|
<nav data-toggle="wy-nav-shift" class="wy-nav-side">
|
||||||
|
<div class="wy-side-scroll">
|
||||||
|
<div class="wy-side-nav-search" {% if theme_style_nav_header_background %} style="background: {{theme_style_nav_header_background}}" {% endif %}>
|
||||||
|
{%- block sidebartitle %}
|
||||||
|
|
||||||
|
{# the logo helper function was removed in Sphinx 6 and deprecated since Sphinx 4 #}
|
||||||
|
{# the master_doc variable was renamed to root_doc in Sphinx 4 (master_doc still exists in later Sphinx versions) #}
|
||||||
|
{%- set _logo_url = logo_url|default(pathto('_static/' + (logo or ""), 1)) %}
|
||||||
|
{%- set _root_doc = root_doc|default(master_doc) %}
|
||||||
|
<a href="{{ pathto(_root_doc) }}"{% if not theme_logo_only %} class="icon icon-home"{% endif %}>
|
||||||
|
{% if not theme_logo_only %}{{ project }}{% endif %}
|
||||||
|
{%- if logo or logo_url %}
|
||||||
|
<img src="{{ _logo_url }}" class="logo" alt="{{ _('Logo') }}"/>
|
||||||
|
{%- endif %}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{%- if READTHEDOCS or DEBUG %}
|
||||||
|
{%- if theme_version_selector or theme_language_selector %}
|
||||||
|
<div class="switch-menus">
|
||||||
|
<div class="version-switch"></div>
|
||||||
|
<div class="language-switch"></div>
|
||||||
|
</div>
|
||||||
|
{%- endif %}
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
|
{%- include "searchbox.html" %}
|
||||||
|
|
||||||
|
{%- endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{%- block navigation %}
|
||||||
|
{#- Translators: This is an ARIA section label for the main navigation menu -#}
|
||||||
|
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="{{ _('Navigation menu') }}">
|
||||||
|
{%- block menu %}
|
||||||
|
{%- set toctree = toctree(maxdepth=theme_navigation_depth|int,
|
||||||
|
collapse=theme_collapse_navigation|tobool,
|
||||||
|
includehidden=theme_includehidden|tobool,
|
||||||
|
titles_only=theme_titles_only|tobool) %}
|
||||||
|
{%- if toctree %}
|
||||||
|
{{ toctree }}
|
||||||
|
{%- else %}
|
||||||
|
<!-- Local TOC -->
|
||||||
|
<div class="local-toc">{{ toc }}</div>
|
||||||
|
{%- endif %}
|
||||||
|
{%- endblock %}
|
||||||
|
</div>
|
||||||
|
{%- endblock %}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
|
||||||
|
|
||||||
|
{#- MOBILE NAV, TRIGGLES SIDE NAV ON TOGGLE #}
|
||||||
|
{#- Translators: This is an ARIA section label for the navigation menu that is visible when viewing the page on mobile devices -#}
|
||||||
|
<nav class="wy-nav-top" aria-label="{{ _('Mobile navigation menu') }}" {% if theme_style_nav_header_background %} style="background: {{theme_style_nav_header_background}}" {% endif %}>
|
||||||
|
{%- block mobile_nav %}
|
||||||
|
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
|
||||||
|
<a href="{{ pathto(master_doc) }}">{{ project }}</a>
|
||||||
|
{%- endblock %}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="wy-nav-content">
|
||||||
|
{%- block content %}
|
||||||
|
{%- if theme_style_external_links|tobool %}
|
||||||
|
<div class="rst-content style-external-links">
|
||||||
|
{%- else %}
|
||||||
|
<div class="rst-content">
|
||||||
|
{%- endif %}
|
||||||
|
{% include "breadcrumbs.html" %}
|
||||||
|
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
|
||||||
|
{%- block document %}
|
||||||
|
<div itemprop="articleBody">
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
{%- if self.comments()|trim %}
|
||||||
|
<div class="articleComments">
|
||||||
|
{%- block comments %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
{%- endif%}
|
||||||
|
</div>
|
||||||
|
{%- endblock %}
|
||||||
|
{% include "footer.html" %}
|
||||||
|
</div>
|
||||||
|
{%- endblock %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
{% include "versions.html" -%}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
jQuery(function () {
|
||||||
|
SphinxRtdTheme.Navigation.enable({{ 'true' if theme_sticky_navigation|tobool else 'false' }});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#- Do not conflict with RTD insertion of analytics script #}
|
||||||
|
{%- if not READTHEDOCS %}
|
||||||
|
{%- if theme_analytics_id %}
|
||||||
|
<!-- Theme Analytics -->
|
||||||
|
<script async src="https://www.googletagmanager.com/gtag/js?id={{ theme_analytics_id }}"></script>
|
||||||
|
<script>
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
|
||||||
|
gtag('config', '{{ theme_analytics_id }}', {
|
||||||
|
'anonymize_ip': {{ 'true' if theme_analytics_anonymize_ip|tobool else 'false' }},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
{%- block footer %}
|
|
||||||
<div class="footer">
|
|
||||||
{%- if show_copyright %}
|
|
||||||
{%- if hasdoc('copyright') %}
|
|
||||||
{% trans path=pathto('copyright'), copyright=copyright|e %}© <a href="{{ path }}">Copyright</a> {{ copyright }}.{% endtrans %}
|
|
||||||
{%- else %}
|
|
||||||
{% trans copyright=copyright|e %}© Copyright {{ copyright }}.{% endtrans %}
|
|
||||||
{%- endif %}
|
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- if last_updated %}
|
{%- endif %}
|
||||||
{% trans last_updated=last_updated|e %}Last updated on {{ last_updated }}.{% endtrans %}
|
|
||||||
{%- endif %}
|
|
||||||
{%- if show_sphinx %}
|
|
||||||
{% trans sphinx_version=sphinx_version|e %}Created using <a href="http://sphinx-doc.org/">Sphinx</a> {{ sphinx_version }}.{% endtrans %}
|
|
||||||
{%- endif %}
|
|
||||||
</div>
|
|
||||||
<p>asdf asdf asdf asdf 22</p>
|
|
||||||
{%- endblock %}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
|
{%- block footer %} {% endblock %}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -39,7 +39,7 @@ as well as the type of underlying hardware. Example:
|
|||||||
"rsa_pubkey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqh…nswIDAQAB\n-----END PUBLIC KEY-----\n"
|
"rsa_pubkey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqh…nswIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
The ``rsa_pubkey`` is optional any only required for certain fatures such as working with reusable
|
The ``rsa_pubkey`` is optional any only required for certain features such as working with reusable
|
||||||
media and NFC cryptography.
|
media and NFC cryptography.
|
||||||
|
|
||||||
Every initialization token can only be used once. On success, you will receive a response containing
|
Every initialization token can only be used once. On success, you will receive a response containing
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ List-level conditional fetching
|
|||||||
If modification checks are not possible with this granularity, you can instead check for the full list.
|
If modification checks are not possible with this granularity, you can instead check for the full list.
|
||||||
In this case, the list of objects may contain a regular HTTP header ``Last-Modified`` with the date of the
|
In this case, the list of objects may contain a regular HTTP header ``Last-Modified`` with the date of the
|
||||||
last modification to any item of that resource. You can then pass this date back in your next request in the
|
last modification to any item of that resource. You can then pass this date back in your next request in the
|
||||||
``If-Modified-Since`` header. If the any object has changed in the meantime, you will receive back a full list
|
``If-Modified-Since`` header. If any object has changed in the meantime, you will receive back a full list
|
||||||
(if something it missing, this means the object has been deleted). If nothing happened, we'll send back a
|
(if something it missing, this means the object has been deleted). If nothing happened, we'll send back a
|
||||||
``304 Not Modified`` return code.
|
``304 Not Modified`` return code.
|
||||||
|
|
||||||
|
|||||||
@@ -46,28 +46,28 @@ Endpoints
|
|||||||
Vary: Accept
|
Vary: Accept
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"count": 3,
|
"count": 3,
|
||||||
"next": null,
|
"next": null,
|
||||||
"previous": null,
|
"previous": null,
|
||||||
"results": [
|
"results": [
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"start": "2025-08-14T22:00:00Z",
|
"start": "2025-08-14T22:00:00Z",
|
||||||
"end": "2025-08-15T00:00:00Z"
|
"end": "2025-08-15T00:00:00Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"start": "2025-08-12T22:00:00Z",
|
"start": "2025-08-12T22:00:00Z",
|
||||||
"end": "2025-08-13T22:00:00Z"
|
"end": "2025-08-13T22:00:00Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 14,
|
"id": 14,
|
||||||
"start": "2025-08-15T22:00:00Z",
|
"start": "2025-08-15T22:00:00Z",
|
||||||
"end": "2025-08-17T22:00:00Z"
|
"end": "2025-08-17T22:00:00Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
:param event: The ``slug`` field of the event to fetch
|
:param event: The ``slug`` field of the event to fetch
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
sphinx==7.4.*
|
sphinx==9.1.*
|
||||||
jinja2==3.1.*
|
sphinx-rtd-theme~=3.1.0
|
||||||
sphinx-rtd-theme
|
sphinxcontrib-httpdomain~=1.8.1
|
||||||
sphinxcontrib-httpdomain
|
sphinxcontrib-images~=1.0.1
|
||||||
sphinxcontrib-images
|
sphinxcontrib-jquery~=4.1
|
||||||
sphinxcontrib-jquery
|
sphinxcontrib-spelling~=8.0.2
|
||||||
sphinxcontrib-spelling==8.*
|
sphinxemoji~=0.3.2
|
||||||
sphinxemoji
|
|
||||||
pyenchant==3.3.*
|
pyenchant==3.3.*
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
-e ../
|
-e ../
|
||||||
sphinx==7.4.*
|
sphinx==9.1.*
|
||||||
jinja2==3.1.*
|
sphinx-rtd-theme~=3.1.0
|
||||||
sphinx-rtd-theme
|
sphinxcontrib-httpdomain~=1.8.1
|
||||||
sphinxcontrib-httpdomain
|
sphinxcontrib-images~=1.0.1
|
||||||
sphinxcontrib-images
|
sphinxcontrib-jquery~=4.1
|
||||||
sphinxcontrib-jquery
|
sphinxcontrib-spelling~=8.0.2
|
||||||
sphinxcontrib-spelling==8.*
|
sphinxemoji~=0.3.2
|
||||||
sphinxemoji
|
|
||||||
pyenchant==3.3.*
|
pyenchant==3.3.*
|
||||||
|
|||||||
+17
-17
@@ -3,7 +3,7 @@ name = "pretix"
|
|||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
description = "Reinventing presales, one ticket at a time"
|
description = "Reinventing presales, one ticket at a time"
|
||||||
readme = "README.rst"
|
readme = "README.rst"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.10"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
keywords = ["tickets", "web", "shop", "ecommerce"]
|
keywords = ["tickets", "web", "shop", "ecommerce"]
|
||||||
authors = [
|
authors = [
|
||||||
@@ -29,17 +29,17 @@ dependencies = [
|
|||||||
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
|
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
|
||||||
"babel",
|
"babel",
|
||||||
"BeautifulSoup4==4.14.*",
|
"BeautifulSoup4==4.14.*",
|
||||||
"bleach==6.2.*",
|
"bleach==6.3.*",
|
||||||
"celery==5.6.*",
|
"celery==5.6.*",
|
||||||
"chardet==5.2.*",
|
"chardet==5.2.*",
|
||||||
"cryptography>=44.0.0",
|
"cryptography>=44.0.0",
|
||||||
"css-inline==0.18.*",
|
"css-inline==0.19.*",
|
||||||
"defusedcsv>=1.1.0",
|
"defusedcsv>=1.1.0",
|
||||||
"dnspython==2.*",
|
"dnspython==2.*",
|
||||||
"Django[argon2]==4.2.*,>=4.2.26",
|
"Django[argon2]==4.2.*,>=4.2.26",
|
||||||
"django-bootstrap3==25.2",
|
"django-bootstrap3==26.1",
|
||||||
"django-compressor==4.5.1",
|
"django-compressor==4.6.0",
|
||||||
"django-countries==7.6.*",
|
"django-countries==8.2.*",
|
||||||
"django-filter==25.1",
|
"django-filter==25.1",
|
||||||
"django-formset-js-improved==0.5.0.4",
|
"django-formset-js-improved==0.5.0.4",
|
||||||
"django-formtools==2.5.1",
|
"django-formtools==2.5.1",
|
||||||
@@ -50,13 +50,13 @@ dependencies = [
|
|||||||
"django-localflavor==5.0",
|
"django-localflavor==5.0",
|
||||||
"django-markup",
|
"django-markup",
|
||||||
"django-oauth-toolkit==2.3.*",
|
"django-oauth-toolkit==2.3.*",
|
||||||
"django-otp==1.6.*",
|
"django-otp==1.7.*",
|
||||||
"django-phonenumber-field==7.3.*",
|
"django-phonenumber-field==8.4.*",
|
||||||
"django-redis==6.0.*",
|
"django-redis==6.0.*",
|
||||||
"django-scopes==2.0.*",
|
"django-scopes==2.0.*",
|
||||||
"django-statici18n==2.6.*",
|
"django-statici18n==2.6.*",
|
||||||
"djangorestframework==3.16.*",
|
"djangorestframework==3.16.*",
|
||||||
"dnspython==2.7.*",
|
"dnspython==2.8.*",
|
||||||
"drf_ujson2==1.7.*",
|
"drf_ujson2==1.7.*",
|
||||||
"geoip2==5.*",
|
"geoip2==5.*",
|
||||||
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||||
@@ -65,7 +65,7 @@ dependencies = [
|
|||||||
"kombu==5.6.*",
|
"kombu==5.6.*",
|
||||||
"libsass==0.23.*",
|
"libsass==0.23.*",
|
||||||
"lxml",
|
"lxml",
|
||||||
"markdown==3.9", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
"markdown==3.10", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
||||||
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
||||||
"mt-940==4.30.*",
|
"mt-940==4.30.*",
|
||||||
"oauthlib==3.3.*",
|
"oauthlib==3.3.*",
|
||||||
@@ -75,24 +75,24 @@ dependencies = [
|
|||||||
"paypal-checkout-serversdk==1.0.*",
|
"paypal-checkout-serversdk==1.0.*",
|
||||||
"PyJWT==2.10.*",
|
"PyJWT==2.10.*",
|
||||||
"phonenumberslite==9.0.*",
|
"phonenumberslite==9.0.*",
|
||||||
"Pillow==11.3.*",
|
"Pillow==12.1.*",
|
||||||
"pretix-plugin-build",
|
"pretix-plugin-build",
|
||||||
"protobuf==6.33.*",
|
"protobuf==6.33.*",
|
||||||
"psycopg2-binary",
|
"psycopg2-binary",
|
||||||
"pycountry",
|
"pycountry",
|
||||||
"pycparser==2.23",
|
"pycparser==2.23",
|
||||||
"pycryptodome==3.23.*",
|
"pycryptodome==3.23.*",
|
||||||
"pypdf==6.4.*",
|
"pypdf==6.5.*",
|
||||||
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
||||||
"python-dateutil==2.9.*",
|
"python-dateutil==2.9.*",
|
||||||
"pytz",
|
"pytz",
|
||||||
"pytz-deprecation-shim==0.1.*",
|
"pytz-deprecation-shim==0.1.*",
|
||||||
"pyuca",
|
"pyuca",
|
||||||
"qrcode==8.2",
|
"qrcode==8.2",
|
||||||
"redis==6.4.*",
|
"redis==7.1.*",
|
||||||
"reportlab==4.4.*",
|
"reportlab==4.4.*",
|
||||||
"requests==2.32.*",
|
"requests==2.32.*",
|
||||||
"sentry-sdk==2.47.*",
|
"sentry-sdk==2.49.*",
|
||||||
"sepaxml==2.7.*",
|
"sepaxml==2.7.*",
|
||||||
"stripe==7.9.*",
|
"stripe==7.9.*",
|
||||||
"text-unidecode==1.*",
|
"text-unidecode==1.*",
|
||||||
@@ -110,10 +110,10 @@ dev = [
|
|||||||
"aiohttp==3.13.*",
|
"aiohttp==3.13.*",
|
||||||
"coverage",
|
"coverage",
|
||||||
"coveralls",
|
"coveralls",
|
||||||
"fakeredis==2.32.*",
|
"fakeredis==2.33.*",
|
||||||
"flake8==7.3.*",
|
"flake8==7.3.*",
|
||||||
"freezegun",
|
"freezegun",
|
||||||
"isort==6.1.*",
|
"isort==7.0.*",
|
||||||
"pep8-naming==0.15.*",
|
"pep8-naming==0.15.*",
|
||||||
"potypo",
|
"potypo",
|
||||||
"pytest-asyncio>=0.24",
|
"pytest-asyncio>=0.24",
|
||||||
@@ -123,7 +123,7 @@ dev = [
|
|||||||
"pytest-mock==3.15.*",
|
"pytest-mock==3.15.*",
|
||||||
"pytest-sugar",
|
"pytest-sugar",
|
||||||
"pytest-xdist==3.8.*",
|
"pytest-xdist==3.8.*",
|
||||||
"pytest==8.4.*",
|
"pytest==9.0.*",
|
||||||
"responses",
|
"responses",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1601,7 +1601,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
order.sales_channel,
|
order.sales_channel,
|
||||||
[
|
[
|
||||||
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.price,
|
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.price,
|
||||||
bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
|
cp.addon_to, cp.is_bundled, pos._voucher_discount)
|
||||||
for cp in order_positions
|
for cp in order_positions
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ from pretix.api.serializers.order import (
|
|||||||
OrderFeeCreateSerializer, OrderPositionCreateSerializer,
|
OrderFeeCreateSerializer, OrderPositionCreateSerializer,
|
||||||
)
|
)
|
||||||
from pretix.base.models import ItemVariation, Order, OrderFee, OrderPosition
|
from pretix.base.models import ItemVariation, Order, OrderFee, OrderPosition
|
||||||
from pretix.base.services.orders import OrderError
|
from pretix.base.services.orders import OrderChangeManager, OrderError
|
||||||
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -82,11 +82,11 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
ocm = self.context['ocm']
|
ocm: OrderChangeManager = self.context['ocm']
|
||||||
check_quotas = self.context.get('check_quotas', True)
|
check_quotas = self.context.get('check_quotas', True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ocm.add_position(
|
new_position = ocm.add_position(
|
||||||
item=validated_data['item'],
|
item=validated_data['item'],
|
||||||
variation=validated_data.get('variation'),
|
variation=validated_data.get('variation'),
|
||||||
price=validated_data.get('price'),
|
price=validated_data.get('price'),
|
||||||
@@ -98,7 +98,7 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
|
|||||||
)
|
)
|
||||||
if self.context.get('commit', True):
|
if self.context.get('commit', True):
|
||||||
ocm.commit(check_quotas=check_quotas)
|
ocm.commit(check_quotas=check_quotas)
|
||||||
return validated_data['order'].positions.order_by('-positionid').first()
|
return new_position.position
|
||||||
else:
|
else:
|
||||||
return OrderPosition() # fake to appease DRF
|
return OrderPosition() # fake to appease DRF
|
||||||
except OrderError as e:
|
except OrderError as e:
|
||||||
@@ -131,7 +131,7 @@ class OrderFeeCreateForExistingOrderSerializer(OrderFeeCreateSerializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
ocm = self.context['ocm']
|
ocm: OrderChangeManager = self.context['ocm']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
f = OrderFee(
|
f = OrderFee(
|
||||||
@@ -146,7 +146,7 @@ class OrderFeeCreateForExistingOrderSerializer(OrderFeeCreateSerializer):
|
|||||||
ocm.add_fee(f)
|
ocm.add_fee(f)
|
||||||
if self.context.get('commit', True):
|
if self.context.get('commit', True):
|
||||||
ocm.commit()
|
ocm.commit()
|
||||||
return validated_data['order'].fees.order_by('-pk').first()
|
return f
|
||||||
else:
|
else:
|
||||||
return OrderFee() # fake to appease DRF
|
return OrderFee() # fake to appease DRF
|
||||||
except OrderError as e:
|
except OrderError as e:
|
||||||
@@ -310,7 +310,7 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
ocm = self.context['ocm']
|
ocm: OrderChangeManager = self.context['ocm']
|
||||||
check_quotas = self.context.get('check_quotas', True)
|
check_quotas = self.context.get('check_quotas', True)
|
||||||
current_seat = {'seat_guid': instance.seat.seat_guid} if instance.seat else None
|
current_seat = {'seat_guid': instance.seat.seat_guid} if instance.seat else None
|
||||||
item = validated_data.get('item', instance.item)
|
item = validated_data.get('item', instance.item)
|
||||||
@@ -399,7 +399,7 @@ class OrderFeeChangeSerializer(serializers.ModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
ocm = self.context['ocm']
|
ocm: OrderChangeManager = self.context['ocm']
|
||||||
value = validated_data.get('value', instance.value)
|
value = validated_data.get('value', instance.value)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -443,6 +443,7 @@ class OrganizerSettingsSerializer(SettingsSerializer):
|
|||||||
'customer_accounts',
|
'customer_accounts',
|
||||||
'customer_accounts_native',
|
'customer_accounts_native',
|
||||||
'customer_accounts_link_by_email',
|
'customer_accounts_link_by_email',
|
||||||
|
'customer_accounts_require_login_for_order_access',
|
||||||
'invoice_regenerate_allowed',
|
'invoice_regenerate_allowed',
|
||||||
'contact_mail',
|
'contact_mail',
|
||||||
'imprint_url',
|
'imprint_url',
|
||||||
|
|||||||
@@ -74,6 +74,11 @@ class ExportersMixin:
|
|||||||
@action(detail=True, methods=['GET'], url_name='download', url_path='download/(?P<asyncid>[^/]+)/(?P<cfid>[^/]+)')
|
@action(detail=True, methods=['GET'], url_name='download', url_path='download/(?P<asyncid>[^/]+)/(?P<cfid>[^/]+)')
|
||||||
def download(self, *args, **kwargs):
|
def download(self, *args, **kwargs):
|
||||||
cf = get_object_or_404(CachedFile, id=kwargs['cfid'])
|
cf = get_object_or_404(CachedFile, id=kwargs['cfid'])
|
||||||
|
if not cf.allowed_for_session(self.request, "exporters-api"):
|
||||||
|
return Response(
|
||||||
|
{'status': 'failed', 'message': 'Unknown file ID or export failed'},
|
||||||
|
status=status.HTTP_410_GONE
|
||||||
|
)
|
||||||
if cf.file:
|
if cf.file:
|
||||||
resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type)
|
resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type)
|
||||||
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename).encode("ascii", "ignore")
|
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename).encode("ascii", "ignore")
|
||||||
@@ -109,7 +114,8 @@ class ExportersMixin:
|
|||||||
serializer = JobRunSerializer(exporter=instance, data=self.request.data, **self.get_serializer_kwargs())
|
serializer = JobRunSerializer(exporter=instance, data=self.request.data, **self.get_serializer_kwargs())
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
cf = CachedFile(web_download=False)
|
cf = CachedFile(web_download=True)
|
||||||
|
cf.bind_to_session(self.request, "exporters-api")
|
||||||
cf.date = now()
|
cf.date = now()
|
||||||
cf.expires = now() + timedelta(hours=24)
|
cf.expires = now() + timedelta(hours=24)
|
||||||
cf.save()
|
cf.save()
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
'variations', 'addons', 'bundles', 'meta_values', 'meta_values__property',
|
'variations', 'addons', 'bundles', 'meta_values', 'meta_values__property',
|
||||||
'variations__meta_values', 'variations__meta_values__property',
|
'variations__meta_values', 'variations__meta_values__property',
|
||||||
'require_membership_types', 'variations__require_membership_types',
|
'require_membership_types', 'variations__require_membership_types',
|
||||||
'limit_sales_channels', 'variations__limit_sales_channels',
|
'limit_sales_channels', 'variations__limit_sales_channels', 'program_times'
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ StaticMapping = namedtuple('StaticMapping', ('id', 'pretix_model', 'external_obj
|
|||||||
|
|
||||||
class OutboundSyncProvider:
|
class OutboundSyncProvider:
|
||||||
max_attempts = 5
|
max_attempts = 5
|
||||||
|
list_field_joiner = "," # set to None to keep native lists in properties
|
||||||
|
|
||||||
def __init__(self, event):
|
def __init__(self, event):
|
||||||
self.event = event
|
self.event = event
|
||||||
@@ -281,7 +282,8 @@ class OutboundSyncProvider:
|
|||||||
'Please update value mapping for field "{field_name}" - option "{val}" not assigned'
|
'Please update value mapping for field "{field_name}" - option "{val}" not assigned'
|
||||||
).format(field_name=key, val=val)])
|
).format(field_name=key, val=val)])
|
||||||
|
|
||||||
val = ",".join(val)
|
if self.list_field_joiner:
|
||||||
|
val = self.list_field_joiner.join(val)
|
||||||
return val
|
return val
|
||||||
|
|
||||||
def get_properties(self, inputs: dict, property_mappings: List[dict]):
|
def get_properties(self, inputs: dict, property_mappings: List[dict]):
|
||||||
|
|||||||
@@ -71,15 +71,20 @@ def assign_properties(
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _add_to_list(out, field_name, current_value, new_item, list_sep):
|
def _add_to_list(out, field_name, current_value, new_item_input, list_sep):
|
||||||
new_item = str(new_item)
|
|
||||||
if list_sep is not None:
|
if list_sep is not None:
|
||||||
new_item = new_item.replace(list_sep, "")
|
new_items = str(new_item_input).split(list_sep)
|
||||||
current_value = current_value.split(list_sep) if current_value else []
|
current_value = current_value.split(list_sep) if current_value else []
|
||||||
elif not isinstance(current_value, (list, tuple)):
|
else:
|
||||||
current_value = [str(current_value)]
|
new_items = [str(new_item_input)]
|
||||||
if new_item not in current_value:
|
if not isinstance(current_value, (list, tuple)):
|
||||||
new_list = current_value + [new_item]
|
current_value = [str(current_value)]
|
||||||
|
|
||||||
|
new_list = list(current_value)
|
||||||
|
for new_item in new_items:
|
||||||
|
if new_item not in current_value:
|
||||||
|
new_list.append(new_item)
|
||||||
|
if new_list != current_value:
|
||||||
if list_sep is not None:
|
if list_sep is not None:
|
||||||
new_list = list_sep.join(new_list)
|
new_list = list_sep.join(new_list)
|
||||||
out[field_name] = new_list
|
out[field_name] = new_list
|
||||||
|
|||||||
@@ -66,8 +66,10 @@ from geoip2.errors import AddressNotFoundError
|
|||||||
from phonenumber_field.formfields import PhoneNumberField
|
from phonenumber_field.formfields import PhoneNumberField
|
||||||
from phonenumber_field.phonenumber import PhoneNumber
|
from phonenumber_field.phonenumber import PhoneNumber
|
||||||
from phonenumber_field.widgets import PhoneNumberPrefixWidget
|
from phonenumber_field.widgets import PhoneNumberPrefixWidget
|
||||||
from phonenumbers import NumberParseException, national_significant_number
|
from phonenumbers import (
|
||||||
from phonenumbers.data import _COUNTRY_CODE_TO_REGION_CODE
|
COUNTRY_CODE_TO_REGION_CODE, REGION_CODE_FOR_NON_GEO_ENTITY,
|
||||||
|
NumberParseException, national_significant_number,
|
||||||
|
)
|
||||||
from PIL import ImageOps
|
from PIL import ImageOps
|
||||||
|
|
||||||
from pretix.base.forms.widgets import (
|
from pretix.base.forms.widgets import (
|
||||||
@@ -305,7 +307,9 @@ class WrappedPhonePrefixSelect(Select):
|
|||||||
choices = [("", "---------")]
|
choices = [("", "---------")]
|
||||||
|
|
||||||
if initial:
|
if initial:
|
||||||
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
|
for prefix, values in COUNTRY_CODE_TO_REGION_CODE.items():
|
||||||
|
if all(v == REGION_CODE_FOR_NON_GEO_ENTITY for v in values):
|
||||||
|
continue
|
||||||
if initial in values:
|
if initial in values:
|
||||||
self.initial = "+%d" % prefix
|
self.initial = "+%d" % prefix
|
||||||
break
|
break
|
||||||
@@ -437,7 +441,9 @@ def guess_phone_prefix_from_request(request, event):
|
|||||||
|
|
||||||
|
|
||||||
def get_phone_prefix(country):
|
def get_phone_prefix(country):
|
||||||
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
|
if country == REGION_CODE_FOR_NON_GEO_ENTITY:
|
||||||
|
return None
|
||||||
|
for prefix, values in COUNTRY_CODE_TO_REGION_CODE.items():
|
||||||
if country in values:
|
if country in values:
|
||||||
return prefix
|
return prefix
|
||||||
return None
|
return None
|
||||||
|
|||||||
+50
-12
@@ -34,14 +34,13 @@
|
|||||||
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from asgiref.local import Local
|
||||||
from babel import localedata
|
from babel import localedata
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
from django.utils.formats import date_format, number_format
|
from django.utils.formats import date_format, number_format
|
||||||
from django.utils.translation import gettext
|
from django.utils.translation import gettext
|
||||||
|
|
||||||
from pretix.base.templatetags.money import money_filter
|
|
||||||
|
|
||||||
from i18nfield.fields import ( # noqa
|
from i18nfield.fields import ( # noqa
|
||||||
I18nCharField, I18nTextarea, I18nTextField, I18nTextInput,
|
I18nCharField, I18nTextarea, I18nTextField, I18nTextInput,
|
||||||
)
|
)
|
||||||
@@ -51,6 +50,9 @@ from i18nfield.strings import LazyI18nString # noqa
|
|||||||
from i18nfield.utils import I18nJSONEncoder # noqa
|
from i18nfield.utils import I18nJSONEncoder # noqa
|
||||||
|
|
||||||
|
|
||||||
|
_active_region = Local()
|
||||||
|
|
||||||
|
|
||||||
class LazyDate:
|
class LazyDate:
|
||||||
def __init__(self, value):
|
def __init__(self, value):
|
||||||
self.value = value
|
self.value = value
|
||||||
@@ -86,6 +88,8 @@ class LazyCurrencyNumber:
|
|||||||
return self.__str__()
|
return self.__str__()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
from pretix.base.templatetags.money import money_filter
|
||||||
|
|
||||||
return money_filter(self.value, self.currency)
|
return money_filter(self.value, self.currency)
|
||||||
|
|
||||||
|
|
||||||
@@ -105,14 +109,41 @@ ALLOWED_LANGUAGES = dict(settings.LANGUAGES)
|
|||||||
|
|
||||||
|
|
||||||
def get_babel_locale():
|
def get_babel_locale():
|
||||||
babel_locale = 'en'
|
# Babel, and therefore also django-phonenumberfield, do not support our custom locales such das de_Informal
|
||||||
# Babel, and therefore django-phonenumberfield, do not support our custom locales such das de_Informal
|
# Also, this returns best-effort region information for number formatting etc
|
||||||
if translation.get_language():
|
current_language = translation.get_language()
|
||||||
if localedata.exists(translation.get_language()):
|
current_region = getattr(_active_region, "value", None)
|
||||||
babel_locale = translation.get_language()
|
|
||||||
elif localedata.exists(translation.get_language()[:2]):
|
# Babel only accepts locales that exist on the system. We try combinations in the following order:
|
||||||
babel_locale = translation.get_language()[:2]
|
# language-languageversion-region
|
||||||
return babel_locale
|
# language-region
|
||||||
|
# language-languageversion
|
||||||
|
# language
|
||||||
|
# fallback to system default
|
||||||
|
# fallback to english
|
||||||
|
|
||||||
|
try_locales = []
|
||||||
|
if current_language:
|
||||||
|
if "-" in current_language:
|
||||||
|
lng_parts = current_language.split("-")
|
||||||
|
if current_region:
|
||||||
|
try_locales.append(f"{lng_parts[0]}_{lng_parts[1].title()}_{current_region.upper()}")
|
||||||
|
try_locales.append(f"{lng_parts[0]}_{current_region.upper()}")
|
||||||
|
try_locales.append(f"{lng_parts[0]}_{lng_parts[1].upper()}")
|
||||||
|
try_locales.append(f"{lng_parts[0]}_{lng_parts[1].title()}")
|
||||||
|
try_locales.append(f"{lng_parts[0]}")
|
||||||
|
else:
|
||||||
|
if current_region:
|
||||||
|
try_locales.append(f"{current_language}_{current_region.upper()}")
|
||||||
|
try_locales.append(f"{current_language}")
|
||||||
|
|
||||||
|
try_locales.append(settings.LANGUAGE_CODE)
|
||||||
|
|
||||||
|
for locale in try_locales:
|
||||||
|
if localedata.exists(locale):
|
||||||
|
return locale
|
||||||
|
|
||||||
|
return "en"
|
||||||
|
|
||||||
|
|
||||||
def get_language_without_region(lng=None):
|
def get_language_without_region(lng=None):
|
||||||
@@ -132,6 +163,10 @@ def get_language_without_region(lng=None):
|
|||||||
return lng
|
return lng
|
||||||
|
|
||||||
|
|
||||||
|
def set_region(region):
|
||||||
|
_active_region.value = region
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def language(lng, region=None):
|
def language(lng, region=None):
|
||||||
"""
|
"""
|
||||||
@@ -143,15 +178,18 @@ def language(lng, region=None):
|
|||||||
formatting. If you pass a ``lng`` that already contains a region, e.g. ``pt-br``, the ``region``
|
formatting. If you pass a ``lng`` that already contains a region, e.g. ``pt-br``, the ``region``
|
||||||
attribute will be ignored.
|
attribute will be ignored.
|
||||||
"""
|
"""
|
||||||
_lng = translation.get_language()
|
lng_before = translation.get_language()
|
||||||
|
region_before = getattr(_active_region, "value", None)
|
||||||
lng = lng or settings.LANGUAGE_CODE
|
lng = lng or settings.LANGUAGE_CODE
|
||||||
if '-' not in lng and region:
|
if '-' not in lng and region:
|
||||||
lng += '-' + region.lower()
|
lng += '-' + region.lower()
|
||||||
translation.activate(lng)
|
translation.activate(lng)
|
||||||
|
_active_region.value = region
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
translation.activate(_lng)
|
translation.activate(lng_before)
|
||||||
|
_active_region.value = region_before
|
||||||
|
|
||||||
|
|
||||||
class LazyLocaleException(Exception):
|
class LazyLocaleException(Exception):
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ from reportlab.lib.styles import ParagraphStyle, StyleSheet1
|
|||||||
from reportlab.lib.units import mm
|
from reportlab.lib.units import mm
|
||||||
from reportlab.pdfbase import pdfmetrics
|
from reportlab.pdfbase import pdfmetrics
|
||||||
from reportlab.pdfbase.pdfmetrics import stringWidth
|
from reportlab.pdfbase.pdfmetrics import stringWidth
|
||||||
from reportlab.pdfbase.ttfonts import TTFont
|
|
||||||
from reportlab.pdfgen.canvas import Canvas
|
from reportlab.pdfgen.canvas import Canvas
|
||||||
from reportlab.platypus import (
|
from reportlab.platypus import (
|
||||||
BaseDocTemplate, Flowable, Frame, KeepTogether, NextPageTemplate,
|
BaseDocTemplate, Flowable, Frame, KeepTogether, NextPageTemplate,
|
||||||
@@ -59,7 +58,8 @@ from pretix.base.services.currencies import SOURCE_NAMES
|
|||||||
from pretix.base.signals import register_invoice_renderers
|
from pretix.base.signals import register_invoice_renderers
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
from pretix.helpers.reportlab import (
|
from pretix.helpers.reportlab import (
|
||||||
FontFallbackParagraph, ThumbnailingImageReader, reshaper,
|
FontFallbackParagraph, ThumbnailingImageReader, register_ttf_font_if_new,
|
||||||
|
reshaper,
|
||||||
)
|
)
|
||||||
from pretix.presale.style import get_fonts
|
from pretix.presale.style import get_fonts
|
||||||
|
|
||||||
@@ -234,25 +234,25 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
|||||||
"""
|
"""
|
||||||
Register fonts with reportlab. By default, this registers the OpenSans font family
|
Register fonts with reportlab. By default, this registers the OpenSans font family
|
||||||
"""
|
"""
|
||||||
pdfmetrics.registerFont(TTFont('OpenSans', finders.find('fonts/OpenSans-Regular.ttf')))
|
register_ttf_font_if_new('OpenSans', finders.find('fonts/OpenSans-Regular.ttf'))
|
||||||
pdfmetrics.registerFont(TTFont('OpenSansIt', finders.find('fonts/OpenSans-Italic.ttf')))
|
register_ttf_font_if_new('OpenSansIt', finders.find('fonts/OpenSans-Italic.ttf'))
|
||||||
pdfmetrics.registerFont(TTFont('OpenSansBd', finders.find('fonts/OpenSans-Bold.ttf')))
|
register_ttf_font_if_new('OpenSansBd', finders.find('fonts/OpenSans-Bold.ttf'))
|
||||||
pdfmetrics.registerFont(TTFont('OpenSansBI', finders.find('fonts/OpenSans-BoldItalic.ttf')))
|
register_ttf_font_if_new('OpenSansBI', finders.find('fonts/OpenSans-BoldItalic.ttf'))
|
||||||
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
|
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
|
||||||
italic='OpenSansIt', boldItalic='OpenSansBI')
|
italic='OpenSansIt', boldItalic='OpenSansBI')
|
||||||
|
|
||||||
for family, styles in get_fonts(event=self.event, pdf_support_required=True).items():
|
for family, styles in get_fonts(event=self.event, pdf_support_required=True).items():
|
||||||
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
|
register_ttf_font_if_new(family, finders.find(styles['regular']['truetype']))
|
||||||
if family == self.event.settings.invoice_renderer_font:
|
if family == self.event.settings.invoice_renderer_font:
|
||||||
self.font_regular = family
|
self.font_regular = family
|
||||||
if 'bold' in styles:
|
if 'bold' in styles:
|
||||||
self.font_bold = family + ' B'
|
self.font_bold = family + ' B'
|
||||||
if 'italic' in styles:
|
if 'italic' in styles:
|
||||||
pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype'])))
|
register_ttf_font_if_new(family + ' I', finders.find(styles['italic']['truetype']))
|
||||||
if 'bold' in styles:
|
if 'bold' in styles:
|
||||||
pdfmetrics.registerFont(TTFont(family + ' B', finders.find(styles['bold']['truetype'])))
|
register_ttf_font_if_new(family + ' B', finders.find(styles['bold']['truetype']))
|
||||||
if 'bolditalic' in styles:
|
if 'bolditalic' in styles:
|
||||||
pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype'])))
|
register_ttf_font_if_new(family + ' B I', finders.find(styles['bolditalic']['truetype']))
|
||||||
|
|
||||||
def _normalize(self, text):
|
def _normalize(self, text):
|
||||||
# reportlab does not support unicode combination characters
|
# reportlab does not support unicode combination characters
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class PeppolIdValidator:
|
|||||||
"0020": "[0-9]{9}",
|
"0020": "[0-9]{9}",
|
||||||
"0201": "[0-9a-zA-Z]{6}",
|
"0201": "[0-9a-zA-Z]{6}",
|
||||||
"0204": "[0-9]{2,12}(-[0-9A-Z]{0,30})?-[0-9]{2}",
|
"0204": "[0-9]{2,12}(-[0-9A-Z]{0,30})?-[0-9]{2}",
|
||||||
"0208": "0[0-9]{9}",
|
"0208": "[01][0-9]{9}",
|
||||||
"0209": ".*",
|
"0209": ".*",
|
||||||
"0210": "[A-Z0-9]+",
|
"0210": "[A-Z0-9]+",
|
||||||
"0211": "IT[0-9]{11}",
|
"0211": "IT[0-9]{11}",
|
||||||
@@ -73,6 +73,9 @@ class PeppolIdValidator:
|
|||||||
"0205": "[A-Z0-9]+",
|
"0205": "[A-Z0-9]+",
|
||||||
"0221": "T[0-9]{13}",
|
"0221": "T[0-9]{13}",
|
||||||
"0230": ".*",
|
"0230": ".*",
|
||||||
|
"0244": "[0-9]{13}",
|
||||||
|
"0245": "[0-9]{10}",
|
||||||
|
"0246": "DE[0-9]{9}(-[0-9]{5})?(\\.[0-9A-Z]{1,8})?",
|
||||||
"9901": ".*",
|
"9901": ".*",
|
||||||
"9902": "[1-9][0-9]{7}",
|
"9902": "[1-9][0-9]{7}",
|
||||||
"9904": "DK[0-9]{8}",
|
"9904": "DK[0-9]{8}",
|
||||||
@@ -120,7 +123,6 @@ class PeppolIdValidator:
|
|||||||
"9951": ".*",
|
"9951": ".*",
|
||||||
"9952": ".*",
|
"9952": ".*",
|
||||||
"9953": ".*",
|
"9953": ".*",
|
||||||
"9954": ".*",
|
|
||||||
"9956": "0[0-9]{9}",
|
"9956": "0[0-9]{9}",
|
||||||
"9957": ".*",
|
"9957": ".*",
|
||||||
"9959": ".*",
|
"9959": ".*",
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ from django.utils.translation.trans_real import (
|
|||||||
parse_accept_lang_header,
|
parse_accept_lang_header,
|
||||||
)
|
)
|
||||||
|
|
||||||
from pretix.base.i18n import get_language_without_region
|
from pretix.base.i18n import get_language_without_region, set_region
|
||||||
from pretix.base.settings import global_settings_object
|
from pretix.base.settings import global_settings_object
|
||||||
from pretix.multidomain.urlreverse import (
|
from pretix.multidomain.urlreverse import (
|
||||||
get_event_domain, get_organizer_domain,
|
get_event_domain, get_organizer_domain,
|
||||||
@@ -92,10 +92,14 @@ class LocaleMiddleware(MiddlewareMixin):
|
|||||||
)
|
)
|
||||||
if '-' not in language and settings_holder.settings.region:
|
if '-' not in language and settings_holder.settings.region:
|
||||||
language += '-' + settings_holder.settings.region
|
language += '-' + settings_holder.settings.region
|
||||||
|
if settings_holder.settings.region:
|
||||||
|
set_region(settings_holder.settings.region)
|
||||||
else:
|
else:
|
||||||
gs = global_settings_object(request)
|
gs = global_settings_object(request)
|
||||||
if '-' not in language and gs.settings.region:
|
if '-' not in language and gs.settings.region:
|
||||||
language += '-' + gs.settings.region
|
language += '-' + gs.settings.region
|
||||||
|
if gs.settings.region:
|
||||||
|
set_region(gs.settings.region)
|
||||||
|
|
||||||
translation.activate(language)
|
translation.activate(language)
|
||||||
request.LANGUAGE_CODE = get_language_without_region()
|
request.LANGUAGE_CODE = get_language_without_region()
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ from django.utils.timezone import now
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_otp.models import Device
|
from django_otp.models import Device
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from webauthn.helpers.structs import PublicKeyCredentialDescriptor
|
|
||||||
|
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.helpers.urls import build_absolute_uri
|
from pretix.helpers.urls import build_absolute_uri
|
||||||
@@ -708,6 +707,8 @@ class U2FDevice(Device):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def webauthndevice(self):
|
def webauthndevice(self):
|
||||||
|
from webauthn.helpers.structs import PublicKeyCredentialDescriptor
|
||||||
|
|
||||||
d = json.loads(self.json_data)
|
d = json.loads(self.json_data)
|
||||||
return PublicKeyCredentialDescriptor(websafe_decode(d['keyHandle']))
|
return PublicKeyCredentialDescriptor(websafe_decode(d['keyHandle']))
|
||||||
|
|
||||||
@@ -737,6 +738,8 @@ class WebAuthnDevice(Device):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def webauthndevice(self):
|
def webauthndevice(self):
|
||||||
|
from webauthn.helpers.structs import PublicKeyCredentialDescriptor
|
||||||
|
|
||||||
return PublicKeyCredentialDescriptor(websafe_decode(self.credential_id))
|
return PublicKeyCredentialDescriptor(websafe_decode(self.credential_id))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -59,6 +59,37 @@ class CachedFile(models.Model):
|
|||||||
web_download = models.BooleanField(default=True) # allow web download, True for backwards compatibility in plugins
|
web_download = models.BooleanField(default=True) # allow web download, True for backwards compatibility in plugins
|
||||||
session_key = models.TextField(null=True, blank=True) # only allow download in this session
|
session_key = models.TextField(null=True, blank=True) # only allow download in this session
|
||||||
|
|
||||||
|
def session_key_for_request(self, request, salt=None):
|
||||||
|
from ...api.models import OAuthAccessToken, OAuthApplication
|
||||||
|
from .devices import Device
|
||||||
|
from .organizer import TeamAPIToken
|
||||||
|
|
||||||
|
if hasattr(request, "auth") and isinstance(request.auth, OAuthAccessToken):
|
||||||
|
k = f'app:{request.auth.application.pk}'
|
||||||
|
elif hasattr(request, "auth") and isinstance(request.auth, OAuthApplication):
|
||||||
|
k = f'app:{request.auth.pk}'
|
||||||
|
elif hasattr(request, "auth") and isinstance(request.auth, TeamAPIToken):
|
||||||
|
k = f'token:{request.auth.pk}'
|
||||||
|
elif hasattr(request, "auth") and isinstance(request.auth, Device):
|
||||||
|
k = f'device:{request.auth.pk}'
|
||||||
|
elif request.session.session_key:
|
||||||
|
k = request.session.session_key
|
||||||
|
else:
|
||||||
|
raise ValueError("No auth method found to bind to")
|
||||||
|
|
||||||
|
if salt:
|
||||||
|
k = f"{k}!{salt}"
|
||||||
|
return k
|
||||||
|
|
||||||
|
def allowed_for_session(self, request, salt=None):
|
||||||
|
return (
|
||||||
|
not self.session_key or
|
||||||
|
self.session_key_for_request(request, salt) == self.session_key
|
||||||
|
)
|
||||||
|
|
||||||
|
def bind_to_session(self, request, salt=None):
|
||||||
|
self.session_key = self.session_key_for_request(request, salt)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_delete, sender=CachedFile)
|
@receiver(post_delete, sender=CachedFile)
|
||||||
def cached_file_delete(sender, instance, **kwargs):
|
def cached_file_delete(sender, instance, **kwargs):
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from pretix.base.decimal import round_decimal
|
|||||||
from pretix.base.models.base import LoggedModel
|
from pretix.base.models.base import LoggedModel
|
||||||
|
|
||||||
PositionInfo = namedtuple('PositionInfo',
|
PositionInfo = namedtuple('PositionInfo',
|
||||||
['item_id', 'subevent_id', 'subevent_date_from', 'line_price_gross', 'is_addon_to',
|
['item_id', 'subevent_id', 'subevent_date_from', 'line_price_gross', 'addon_to',
|
||||||
'voucher_discount'])
|
'voucher_discount'])
|
||||||
|
|
||||||
|
|
||||||
@@ -279,6 +279,42 @@ class Discount(LoggedModel):
|
|||||||
for idx in condition_idx_group:
|
for idx in condition_idx_group:
|
||||||
collect_potential_discounts[idx] = [(self, inf, -1, subevent_id)]
|
collect_potential_discounts[idx] = [(self, inf, -1, subevent_id)]
|
||||||
|
|
||||||
|
def _addon_idx(self, positions, idx):
|
||||||
|
"""
|
||||||
|
If we have the following cart:
|
||||||
|
|
||||||
|
- Main product
|
||||||
|
- 10x Addon product 5€
|
||||||
|
- Main product
|
||||||
|
- 10x Addon product 5€
|
||||||
|
|
||||||
|
And we have a discount rule that grants "every 10th product is free", people tend to expect
|
||||||
|
|
||||||
|
- Main product
|
||||||
|
- 9x Addon product 5€
|
||||||
|
- 1x Addon product free
|
||||||
|
- Main product
|
||||||
|
- 9x Addon product 5€
|
||||||
|
- 1x Addon product free
|
||||||
|
|
||||||
|
And get confused if they get
|
||||||
|
|
||||||
|
- Main product
|
||||||
|
- 8x Addon product 5€
|
||||||
|
- 2x Addon product free
|
||||||
|
- Main product
|
||||||
|
- 10x Addon product 5€
|
||||||
|
|
||||||
|
Even if the result is the same. Therefore, we sort positions in the cart not only by price, but also by their
|
||||||
|
relative index within their addon group. This is only a heuristic and there are *still* scenarios where the more
|
||||||
|
unexpected version happens, e.g. if prices are different. We need to accept this as long as discounts work on
|
||||||
|
cart level and not on addon-group level, but this simple sorting reduces the number of support issues by making
|
||||||
|
the weird case less likely.
|
||||||
|
"""
|
||||||
|
if not positions[idx].addon_to:
|
||||||
|
return 0
|
||||||
|
return len([1 for i, p in positions.items() if i < idx and p.addon_to == positions[idx].addon_to])
|
||||||
|
|
||||||
def _apply_min_count(self, positions, condition_idx_group, benefit_idx_group, result, collect_potential_discounts, subevent_id):
|
def _apply_min_count(self, positions, condition_idx_group, benefit_idx_group, result, collect_potential_discounts, subevent_id):
|
||||||
if len(condition_idx_group) < self.condition_min_count:
|
if len(condition_idx_group) < self.condition_min_count:
|
||||||
return
|
return
|
||||||
@@ -288,8 +324,8 @@ class Discount(LoggedModel):
|
|||||||
|
|
||||||
if self.benefit_only_apply_to_cheapest_n_matches:
|
if self.benefit_only_apply_to_cheapest_n_matches:
|
||||||
# sort by line_price
|
# sort by line_price
|
||||||
condition_idx_group = sorted(condition_idx_group, key=lambda idx: (positions[idx].line_price_gross, -idx))
|
condition_idx_group = sorted(condition_idx_group, key=lambda idx: (positions[idx].line_price_gross, self._addon_idx(positions, idx), -idx))
|
||||||
benefit_idx_group = sorted(benefit_idx_group, key=lambda idx: (positions[idx].line_price_gross, -idx))
|
benefit_idx_group = sorted(benefit_idx_group, key=lambda idx: (positions[idx].line_price_gross, self._addon_idx(positions, idx), -idx))
|
||||||
|
|
||||||
# Prevent over-consuming of items, i.e. if our discount is "buy 2, get 1 free", we only
|
# Prevent over-consuming of items, i.e. if our discount is "buy 2, get 1 free", we only
|
||||||
# want to match multiples of 3
|
# want to match multiples of 3
|
||||||
@@ -434,7 +470,7 @@ class Discount(LoggedModel):
|
|||||||
for idx, p in positions.items():
|
for idx, p in positions.items():
|
||||||
subevent_to_idx[p.subevent_id].append(idx)
|
subevent_to_idx[p.subevent_id].append(idx)
|
||||||
for v in subevent_to_idx.values():
|
for v in subevent_to_idx.values():
|
||||||
v.sort(key=lambda idx: positions[idx].line_price_gross)
|
v.sort(key=lambda idx: (positions[idx].line_price_gross, self._addon_idx(positions, idx)))
|
||||||
subevent_order = sorted(list(subevent_to_idx.keys()), key=lambda s: len(subevent_to_idx[s]), reverse=True)
|
subevent_order = sorted(list(subevent_to_idx.keys()), key=lambda s: len(subevent_to_idx[s]), reverse=True)
|
||||||
|
|
||||||
# Build groups of exactly condition_min_count distinct subevents
|
# Build groups of exactly condition_min_count distinct subevents
|
||||||
@@ -458,7 +494,7 @@ class Discount(LoggedModel):
|
|||||||
|
|
||||||
# Sort the list by prices, then pick one. For "buy 2 get 1 free" we apply a "pick 1 from the start
|
# Sort the list by prices, then pick one. For "buy 2 get 1 free" we apply a "pick 1 from the start
|
||||||
# and 2 from the end" scheme to optimize price distribution among groups
|
# and 2 from the end" scheme to optimize price distribution among groups
|
||||||
candidates = sorted(candidates, key=lambda idx: positions[idx].line_price_gross)
|
candidates = sorted(candidates, key=lambda idx: (positions[idx].line_price_gross, self._addon_idx(positions, idx)))
|
||||||
if len(current_group) < (self.benefit_only_apply_to_cheapest_n_matches or 0):
|
if len(current_group) < (self.benefit_only_apply_to_cheapest_n_matches or 0):
|
||||||
candidate = candidates[0]
|
candidate = candidates[0]
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -141,8 +141,9 @@ class LogEntry(models.Model):
|
|||||||
|
|
||||||
log_entry_type, meta = log_entry_types.get(action_type=self.action_type)
|
log_entry_type, meta = log_entry_types.get(action_type=self.action_type)
|
||||||
if log_entry_type:
|
if log_entry_type:
|
||||||
|
sender = self.event if self.event else self.organizer
|
||||||
link_info = log_entry_type.get_object_link_info(self)
|
link_info = log_entry_type.get_object_link_info(self)
|
||||||
if is_app_active(self.event, meta['plugin']):
|
if is_app_active(sender, meta['plugin']):
|
||||||
return make_link(link_info, log_entry_type.object_link_wrapper)
|
return make_link(link_info, log_entry_type.object_link_wrapper)
|
||||||
else:
|
else:
|
||||||
return make_link(link_info, log_entry_type.object_link_wrapper, is_active=False,
|
return make_link(link_info, log_entry_type.object_link_wrapper, is_active=False,
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
import json
|
import json
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
import jsonschema
|
|
||||||
from django.contrib.staticfiles import finders
|
from django.contrib.staticfiles import finders
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@@ -38,6 +37,8 @@ from pretix.base.models import Event, Item, LoggedModel, Organizer, SubEvent
|
|||||||
@deconstructible
|
@deconstructible
|
||||||
class SeatingPlanLayoutValidator:
|
class SeatingPlanLayoutValidator:
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
|
import jsonschema
|
||||||
|
|
||||||
if not isinstance(value, dict):
|
if not isinstance(value, dict):
|
||||||
try:
|
try:
|
||||||
val = json.loads(value)
|
val = json.loads(value)
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import json
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import jsonschema
|
|
||||||
from django.contrib.staticfiles import finders
|
from django.contrib.staticfiles import finders
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
@@ -298,6 +297,8 @@ def cc_to_vat_prefix(country_code):
|
|||||||
@deconstructible
|
@deconstructible
|
||||||
class CustomRulesValidator:
|
class CustomRulesValidator:
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
|
import jsonschema
|
||||||
|
|
||||||
if not isinstance(value, dict):
|
if not isinstance(value, dict):
|
||||||
try:
|
try:
|
||||||
val = json.loads(value)
|
val = json.loads(value)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ from pretix.base.email import get_email_context
|
|||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import User, Voucher
|
from pretix.base.models import User, Voucher
|
||||||
from pretix.base.services.mail import SendMailException, mail, render_mail
|
from pretix.base.services.mail import SendMailException, mail, render_mail
|
||||||
|
from pretix.helpers import OF_SELF
|
||||||
|
|
||||||
from ...helpers.format import format_map
|
from ...helpers.format import format_map
|
||||||
from ...helpers.names import build_name
|
from ...helpers.names import build_name
|
||||||
@@ -185,44 +186,47 @@ class WaitingListEntry(LoggedModel):
|
|||||||
if not free_seats:
|
if not free_seats:
|
||||||
raise WaitingListException(_('No seat with this product is currently available.'))
|
raise WaitingListException(_('No seat with this product is currently available.'))
|
||||||
|
|
||||||
if self.voucher:
|
|
||||||
raise WaitingListException(_('A voucher has already been sent to this person.'))
|
|
||||||
if '@' not in self.email:
|
if '@' not in self.email:
|
||||||
raise WaitingListException(_('This entry is anonymized and can no longer be used.'))
|
raise WaitingListException(_('This entry is anonymized and can no longer be used.'))
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
e = self.email
|
locked_wle = WaitingListEntry.objects.select_for_update(of=OF_SELF).get(pk=self.pk)
|
||||||
if self.name:
|
if locked_wle.voucher:
|
||||||
e += ' / ' + self.name
|
raise WaitingListException(_('A voucher has already been sent to this person.'))
|
||||||
|
e = locked_wle.email
|
||||||
|
if locked_wle.name:
|
||||||
|
e += ' / ' + locked_wle.name
|
||||||
v = Voucher.objects.create(
|
v = Voucher.objects.create(
|
||||||
event=self.event,
|
event=locked_wle.event,
|
||||||
max_usages=1,
|
max_usages=1,
|
||||||
valid_until=now() + timedelta(hours=self.event.settings.waiting_list_hours),
|
valid_until=now() + timedelta(hours=locked_wle.event.settings.waiting_list_hours),
|
||||||
item=self.item,
|
item=locked_wle.item,
|
||||||
variation=self.variation,
|
variation=locked_wle.variation,
|
||||||
tag='waiting-list',
|
tag='waiting-list',
|
||||||
comment=_('Automatically created from waiting list entry for {email}').format(
|
comment=_('Automatically created from waiting list entry for {email}').format(
|
||||||
email=e
|
email=e
|
||||||
),
|
),
|
||||||
block_quota=True,
|
block_quota=True,
|
||||||
subevent=self.subevent,
|
subevent=locked_wle.subevent,
|
||||||
)
|
)
|
||||||
v.log_action('pretix.voucher.added', {
|
v.log_action('pretix.voucher.added', {
|
||||||
'item': self.item.pk,
|
'item': locked_wle.item.pk,
|
||||||
'variation': self.variation.pk if self.variation else None,
|
'variation': locked_wle.variation.pk if locked_wle.variation else None,
|
||||||
'tag': 'waiting-list',
|
'tag': 'waiting-list',
|
||||||
'block_quota': True,
|
'block_quota': True,
|
||||||
'valid_until': v.valid_until.isoformat(),
|
'valid_until': v.valid_until.isoformat(),
|
||||||
'max_usages': 1,
|
'max_usages': 1,
|
||||||
'subevent': self.subevent.pk if self.subevent else None,
|
'subevent': locked_wle.subevent.pk if locked_wle.subevent else None,
|
||||||
'source': 'waitinglist',
|
'source': 'waitinglist',
|
||||||
}, user=user, auth=auth)
|
}, user=user, auth=auth)
|
||||||
v.log_action('pretix.voucher.added.waitinglist', {
|
v.log_action('pretix.voucher.added.waitinglist', {
|
||||||
'email': self.email,
|
'email': locked_wle.email,
|
||||||
'waitinglistentry': self.pk,
|
'waitinglistentry': locked_wle.pk,
|
||||||
}, user=user, auth=auth)
|
}, user=user, auth=auth)
|
||||||
self.voucher = v
|
locked_wle.voucher = v
|
||||||
self.save()
|
locked_wle.save()
|
||||||
|
|
||||||
|
self.refresh_from_db()
|
||||||
|
|
||||||
with language(self.locale, self.event.settings.region):
|
with language(self.locale, self.event.settings.region):
|
||||||
self.send_mail(
|
self.send_mail(
|
||||||
|
|||||||
+13
-12
@@ -47,7 +47,6 @@ from collections import OrderedDict, defaultdict
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
import jsonschema
|
|
||||||
import pypdf
|
import pypdf
|
||||||
import pypdf.generic
|
import pypdf.generic
|
||||||
import reportlab.rl_config
|
import reportlab.rl_config
|
||||||
@@ -72,9 +71,7 @@ from reportlab.lib.colors import Color
|
|||||||
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
|
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
|
||||||
from reportlab.lib.styles import ParagraphStyle
|
from reportlab.lib.styles import ParagraphStyle
|
||||||
from reportlab.lib.units import mm
|
from reportlab.lib.units import mm
|
||||||
from reportlab.pdfbase import pdfmetrics
|
|
||||||
from reportlab.pdfbase.pdfmetrics import getAscentDescent
|
from reportlab.pdfbase.pdfmetrics import getAscentDescent
|
||||||
from reportlab.pdfbase.ttfonts import TTFont
|
|
||||||
from reportlab.pdfgen.canvas import Canvas
|
from reportlab.pdfgen.canvas import Canvas
|
||||||
from reportlab.platypus import Paragraph
|
from reportlab.platypus import Paragraph
|
||||||
|
|
||||||
@@ -85,7 +82,9 @@ from pretix.base.signals import layout_image_variables, layout_text_variables
|
|||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
from pretix.base.templatetags.phone_format import phone_format
|
from pretix.base.templatetags.phone_format import phone_format
|
||||||
from pretix.helpers.daterange import datetimerange
|
from pretix.helpers.daterange import datetimerange
|
||||||
from pretix.helpers.reportlab import ThumbnailingImageReader, reshaper
|
from pretix.helpers.reportlab import (
|
||||||
|
ThumbnailingImageReader, register_ttf_font_if_new, reshaper,
|
||||||
|
)
|
||||||
from pretix.presale.style import get_fonts
|
from pretix.presale.style import get_fonts
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -795,19 +794,19 @@ class Renderer:
|
|||||||
def _register_fonts(cls, event: Event = None):
|
def _register_fonts(cls, event: Event = None):
|
||||||
if hasattr(cls, '_fonts_registered'):
|
if hasattr(cls, '_fonts_registered'):
|
||||||
return
|
return
|
||||||
pdfmetrics.registerFont(TTFont('Open Sans', finders.find('fonts/OpenSans-Regular.ttf')))
|
register_ttf_font_if_new('Open Sans', finders.find('fonts/OpenSans-Regular.ttf'))
|
||||||
pdfmetrics.registerFont(TTFont('Open Sans I', finders.find('fonts/OpenSans-Italic.ttf')))
|
register_ttf_font_if_new('Open Sans I', finders.find('fonts/OpenSans-Italic.ttf'))
|
||||||
pdfmetrics.registerFont(TTFont('Open Sans B', finders.find('fonts/OpenSans-Bold.ttf')))
|
register_ttf_font_if_new('Open Sans B', finders.find('fonts/OpenSans-Bold.ttf'))
|
||||||
pdfmetrics.registerFont(TTFont('Open Sans B I', finders.find('fonts/OpenSans-BoldItalic.ttf')))
|
register_ttf_font_if_new('Open Sans B I', finders.find('fonts/OpenSans-BoldItalic.ttf'))
|
||||||
|
|
||||||
for family, styles in get_fonts(event, pdf_support_required=True).items():
|
for family, styles in get_fonts(event, pdf_support_required=True).items():
|
||||||
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
|
register_ttf_font_if_new(family, finders.find(styles['regular']['truetype']))
|
||||||
if 'italic' in styles:
|
if 'italic' in styles:
|
||||||
pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype'])))
|
register_ttf_font_if_new(family + ' I', finders.find(styles['italic']['truetype']))
|
||||||
if 'bold' in styles:
|
if 'bold' in styles:
|
||||||
pdfmetrics.registerFont(TTFont(family + ' B', finders.find(styles['bold']['truetype'])))
|
register_ttf_font_if_new(family + ' B', finders.find(styles['bold']['truetype']))
|
||||||
if 'bolditalic' in styles:
|
if 'bolditalic' in styles:
|
||||||
pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype'])))
|
register_ttf_font_if_new(family + ' B I', finders.find(styles['bolditalic']['truetype']))
|
||||||
|
|
||||||
cls._fonts_registered = True
|
cls._fonts_registered = True
|
||||||
|
|
||||||
@@ -1311,6 +1310,8 @@ def _correct_page_media_box(page: pypdf.PageObject):
|
|||||||
@deconstructible
|
@deconstructible
|
||||||
class PdfLayoutValidator:
|
class PdfLayoutValidator:
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
|
import jsonschema
|
||||||
|
|
||||||
if not isinstance(value, dict):
|
if not isinstance(value, dict):
|
||||||
try:
|
try:
|
||||||
val = json.loads(value)
|
val = json.loads(value)
|
||||||
|
|||||||
@@ -97,6 +97,10 @@ class CartError(Exception):
|
|||||||
super().__init__(msg)
|
super().__init__(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class CartPositionError(CartError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
error_messages = {
|
error_messages = {
|
||||||
'busy': gettext_lazy(
|
'busy': gettext_lazy(
|
||||||
'We were not able to process your request completely as the '
|
'We were not able to process your request completely as the '
|
||||||
@@ -106,6 +110,9 @@ error_messages = {
|
|||||||
'unknown_position': gettext_lazy('Unknown cart position.'),
|
'unknown_position': gettext_lazy('Unknown cart position.'),
|
||||||
'subevent_required': pgettext_lazy('subevent', 'No date was specified.'),
|
'subevent_required': pgettext_lazy('subevent', 'No date was specified.'),
|
||||||
'not_for_sale': gettext_lazy('You selected a product which is not available for sale.'),
|
'not_for_sale': gettext_lazy('You selected a product which is not available for sale.'),
|
||||||
|
'positions_removed': gettext_lazy(
|
||||||
|
'Some products can no longer be purchased and have been removed from your cart for the following reason: %s'
|
||||||
|
),
|
||||||
'unavailable': gettext_lazy(
|
'unavailable': gettext_lazy(
|
||||||
'Some of the products you selected are no longer available. '
|
'Some of the products you selected are no longer available. '
|
||||||
'Please see below for details.'
|
'Please see below for details.'
|
||||||
@@ -258,6 +265,138 @@ def _get_voucher_availability(event, voucher_use_diff, now_dt, exclude_position_
|
|||||||
return vouchers_ok, _voucher_depend_on_cart
|
return vouchers_ok, _voucher_depend_on_cart
|
||||||
|
|
||||||
|
|
||||||
|
def _check_position_constraints(
|
||||||
|
event: Event, item: Item, variation: ItemVariation, voucher: Voucher, subevent: SubEvent,
|
||||||
|
seat: Seat, sales_channel: SalesChannel, already_in_cart: bool, cart_is_expired: bool, real_now_dt: datetime,
|
||||||
|
item_requires_seat: bool, is_addon: bool, is_bundled: bool,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Checks if a cart position with the given constraints can still be sold. This checks configuration and time-based
|
||||||
|
constraints of item, subevent, and voucher.
|
||||||
|
|
||||||
|
It does NOT
|
||||||
|
- check if quota/voucher/seat are still available
|
||||||
|
- check prices
|
||||||
|
- check memberships
|
||||||
|
- perform any checks that go beyond the single line (like item.max_per_order)
|
||||||
|
"""
|
||||||
|
time_machine_now_dt = time_machine_now(real_now_dt)
|
||||||
|
# Item or variation disabled
|
||||||
|
# Item disabled or unavailable by time
|
||||||
|
if not item.is_available(time_machine_now_dt) or (variation and not variation.is_available(time_machine_now_dt)):
|
||||||
|
raise CartPositionError(error_messages['unavailable'])
|
||||||
|
|
||||||
|
# Invalid media policy for online sale
|
||||||
|
if item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW):
|
||||||
|
mt = MEDIA_TYPES[item.media_type]
|
||||||
|
if not mt.medium_created_by_server:
|
||||||
|
raise CartPositionError(error_messages['media_usage_not_implemented'])
|
||||||
|
elif item.media_policy == Item.MEDIA_POLICY_REUSE:
|
||||||
|
raise CartPositionError(error_messages['media_usage_not_implemented'])
|
||||||
|
|
||||||
|
# Item removed from sales channel
|
||||||
|
if not item.all_sales_channels:
|
||||||
|
if sales_channel.identifier not in (s.identifier for s in item.limit_sales_channels.all()):
|
||||||
|
raise CartPositionError(error_messages['unavailable'])
|
||||||
|
|
||||||
|
# Variation removed from sales channel
|
||||||
|
if variation and not variation.all_sales_channels:
|
||||||
|
if sales_channel.identifier not in (s.identifier for s in variation.limit_sales_channels.all()):
|
||||||
|
raise CartPositionError(error_messages['unavailable'])
|
||||||
|
|
||||||
|
# Item disabled or unavailable by time in subevent
|
||||||
|
if subevent and item.pk in subevent.item_overrides and not subevent.item_overrides[item.pk].is_available(time_machine_now_dt):
|
||||||
|
raise CartPositionError(error_messages['not_for_sale'])
|
||||||
|
|
||||||
|
# Variation disabled or unavailable by time in subevent
|
||||||
|
if subevent and variation and variation.pk in subevent.var_overrides and \
|
||||||
|
not subevent.var_overrides[variation.pk].is_available(time_machine_now_dt):
|
||||||
|
raise CartPositionError(error_messages['not_for_sale'])
|
||||||
|
|
||||||
|
# Item requires a variation (should never happen)
|
||||||
|
if item.has_variations and not variation:
|
||||||
|
raise CartPositionError(error_messages['not_for_sale'])
|
||||||
|
|
||||||
|
# Variation belongs to wrong item (should never happen)
|
||||||
|
if variation and variation.item_id != item.pk:
|
||||||
|
raise CartPositionError(error_messages['not_for_sale'])
|
||||||
|
|
||||||
|
# Voucher does not apply to product
|
||||||
|
if voucher and not voucher.applies_to(item, variation):
|
||||||
|
raise CartPositionError(error_messages['voucher_invalid_item'])
|
||||||
|
|
||||||
|
# Voucher does not apply to seat
|
||||||
|
if voucher and voucher.seat and voucher.seat != seat:
|
||||||
|
raise CartPositionError(error_messages['voucher_invalid_seat'])
|
||||||
|
|
||||||
|
# Voucher does not apply to subevent
|
||||||
|
if voucher and voucher.subevent_id and voucher.subevent_id != subevent.pk:
|
||||||
|
raise CartPositionError(error_messages['voucher_invalid_subevent'])
|
||||||
|
|
||||||
|
# Voucher expired
|
||||||
|
if voucher and voucher.valid_until and voucher.valid_until < time_machine_now_dt:
|
||||||
|
raise CartPositionError(error_messages['voucher_expired'])
|
||||||
|
|
||||||
|
# Subevent has been disabled
|
||||||
|
if subevent and not subevent.active:
|
||||||
|
raise CartPositionError(error_messages['inactive_subevent'])
|
||||||
|
|
||||||
|
# Subevent sale not started
|
||||||
|
if subevent and subevent.effective_presale_start and time_machine_now_dt < subevent.effective_presale_start:
|
||||||
|
raise CartPositionError(error_messages['not_started'])
|
||||||
|
|
||||||
|
# Subevent sale has ended
|
||||||
|
if subevent and subevent.presale_has_ended:
|
||||||
|
raise CartPositionError(error_messages['ended'])
|
||||||
|
|
||||||
|
# Payment for subevent no longer possible
|
||||||
|
if subevent:
|
||||||
|
tlv = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
||||||
|
if tlv:
|
||||||
|
term_last = make_aware(datetime.combine(
|
||||||
|
tlv.datetime(subevent).date(),
|
||||||
|
time(hour=23, minute=59, second=59)
|
||||||
|
), event.timezone)
|
||||||
|
if term_last < time_machine_now_dt:
|
||||||
|
raise CartPositionError(error_messages['payment_ended'])
|
||||||
|
|
||||||
|
# Seat required but no seat given
|
||||||
|
if item_requires_seat and not seat:
|
||||||
|
raise CartPositionError(error_messages['seat_invalid'])
|
||||||
|
|
||||||
|
# Seat given but no seat required
|
||||||
|
if seat and not item_requires_seat:
|
||||||
|
raise CartPositionError(error_messages['seat_forbidden'])
|
||||||
|
|
||||||
|
# Item requires to be add-on but is top-level position
|
||||||
|
if item.category and item.category.is_addon and not is_addon:
|
||||||
|
raise CartPositionError(error_messages['addon_only'])
|
||||||
|
|
||||||
|
# Item requires bundling but is top-level position
|
||||||
|
if item.require_bundling and not is_bundled:
|
||||||
|
raise CartPositionError(error_messages['bundled_only'])
|
||||||
|
|
||||||
|
# Seat for wrong product
|
||||||
|
if seat and seat.product != item:
|
||||||
|
raise CartPositionError(error_messages['seat_invalid'])
|
||||||
|
|
||||||
|
# Seat blocked
|
||||||
|
if seat and seat.blocked and sales_channel.identifier not in event.settings.seating_allow_blocked_seats_for_channel:
|
||||||
|
raise CartPositionError(error_messages['seat_invalid'])
|
||||||
|
|
||||||
|
# Item requires voucher but no voucher given
|
||||||
|
if item.require_voucher and voucher is None and not is_bundled:
|
||||||
|
raise CartPositionError(error_messages['voucher_required'])
|
||||||
|
|
||||||
|
# Item or variation is hidden without voucher but no voucher is given
|
||||||
|
if (
|
||||||
|
(item.hide_without_voucher or (variation and variation.hide_without_voucher)) and
|
||||||
|
(voucher is None or not voucher.show_hidden_items) and
|
||||||
|
not is_bundled
|
||||||
|
):
|
||||||
|
raise CartPositionError(error_messages['voucher_required'])
|
||||||
|
|
||||||
|
|
||||||
class CartManager:
|
class CartManager:
|
||||||
AddOperation = namedtuple('AddOperation', ('count', 'item', 'variation', 'voucher', 'quotas',
|
AddOperation = namedtuple('AddOperation', ('count', 'item', 'variation', 'voucher', 'quotas',
|
||||||
'addon_to', 'subevent', 'bundled', 'seat', 'listed_price',
|
'addon_to', 'subevent', 'bundled', 'seat', 'listed_price',
|
||||||
@@ -294,6 +433,7 @@ class CartManager:
|
|||||||
self._widget_data = widget_data or {}
|
self._widget_data = widget_data or {}
|
||||||
self._sales_channel = sales_channel
|
self._sales_channel = sales_channel
|
||||||
self.num_extended_positions = 0
|
self.num_extended_positions = 0
|
||||||
|
self.price_change_for_extended = False
|
||||||
|
|
||||||
if reservation_time:
|
if reservation_time:
|
||||||
self._reservation_time = reservation_time
|
self._reservation_time = reservation_time
|
||||||
@@ -421,14 +561,14 @@ class CartManager:
|
|||||||
if cartsize > limit:
|
if cartsize > limit:
|
||||||
raise CartError(error_messages['max_items'] % limit)
|
raise CartError(error_messages['max_items'] % limit)
|
||||||
|
|
||||||
def _check_item_constraints(self, op, current_ops=[]):
|
def _check_item_constraints(self, op):
|
||||||
if isinstance(op, (self.AddOperation, self.ExtendOperation)):
|
if isinstance(op, (self.AddOperation, self.ExtendOperation)):
|
||||||
if not (
|
if not (
|
||||||
(isinstance(op, self.AddOperation) and op.addon_to == 'FAKE') or
|
(isinstance(op, self.AddOperation) and op.addon_to == 'FAKE') or
|
||||||
(isinstance(op, self.ExtendOperation) and op.position.is_bundled)
|
(isinstance(op, self.ExtendOperation) and op.position.is_bundled)
|
||||||
):
|
):
|
||||||
if op.item.require_voucher and op.voucher is None:
|
if op.item.require_voucher and op.voucher is None:
|
||||||
if getattr(op, 'voucher_ignored', False):
|
if getattr(op, 'voucher_ignored', False): # todo??
|
||||||
raise CartError(error_messages['voucher_redeemed'])
|
raise CartError(error_messages['voucher_redeemed'])
|
||||||
raise CartError(error_messages['voucher_required'])
|
raise CartError(error_messages['voucher_required'])
|
||||||
|
|
||||||
@@ -440,88 +580,39 @@ class CartManager:
|
|||||||
raise CartError(error_messages['voucher_redeemed'])
|
raise CartError(error_messages['voucher_redeemed'])
|
||||||
raise CartError(error_messages['voucher_required'])
|
raise CartError(error_messages['voucher_required'])
|
||||||
|
|
||||||
if not op.item.is_available() or (op.variation and not op.variation.is_available()):
|
if op.seat and op.count > 1:
|
||||||
raise CartError(error_messages['unavailable'])
|
|
||||||
|
|
||||||
if op.item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW):
|
|
||||||
mt = MEDIA_TYPES[op.item.media_type]
|
|
||||||
if not mt.medium_created_by_server:
|
|
||||||
raise CartError(error_messages['media_usage_not_implemented'])
|
|
||||||
elif op.item.media_policy == Item.MEDIA_POLICY_REUSE:
|
|
||||||
raise CartError(error_messages['media_usage_not_implemented'])
|
|
||||||
|
|
||||||
if not op.item.all_sales_channels:
|
|
||||||
if self._sales_channel.identifier not in (s.identifier for s in op.item.limit_sales_channels.all()):
|
|
||||||
raise CartError(error_messages['unavailable'])
|
|
||||||
|
|
||||||
if op.variation and not op.variation.all_sales_channels:
|
|
||||||
if self._sales_channel.identifier not in (s.identifier for s in op.variation.limit_sales_channels.all()):
|
|
||||||
raise CartError(error_messages['unavailable'])
|
|
||||||
|
|
||||||
if op.subevent and op.item.pk in op.subevent.item_overrides and not op.subevent.item_overrides[op.item.pk].is_available():
|
|
||||||
raise CartError(error_messages['not_for_sale'])
|
|
||||||
|
|
||||||
if op.subevent and op.variation and op.variation.pk in op.subevent.var_overrides and \
|
|
||||||
not op.subevent.var_overrides[op.variation.pk].is_available():
|
|
||||||
raise CartError(error_messages['not_for_sale'])
|
|
||||||
|
|
||||||
if op.item.has_variations and not op.variation:
|
|
||||||
raise CartError(error_messages['not_for_sale'])
|
|
||||||
|
|
||||||
if op.variation and op.variation.item_id != op.item.pk:
|
|
||||||
raise CartError(error_messages['not_for_sale'])
|
|
||||||
|
|
||||||
if op.voucher and not op.voucher.applies_to(op.item, op.variation):
|
|
||||||
raise CartError(error_messages['voucher_invalid_item'])
|
|
||||||
|
|
||||||
if op.voucher and op.voucher.seat and op.voucher.seat != op.seat:
|
|
||||||
raise CartError(error_messages['voucher_invalid_seat'])
|
|
||||||
|
|
||||||
if op.voucher and op.voucher.subevent_id and op.voucher.subevent_id != op.subevent.pk:
|
|
||||||
raise CartError(error_messages['voucher_invalid_subevent'])
|
|
||||||
|
|
||||||
if op.subevent and not op.subevent.active:
|
|
||||||
raise CartError(error_messages['inactive_subevent'])
|
|
||||||
|
|
||||||
if op.subevent and op.subevent.presale_start and time_machine_now(self.real_now_dt) < op.subevent.presale_start:
|
|
||||||
raise CartError(error_messages['not_started'])
|
|
||||||
|
|
||||||
if op.subevent and op.subevent.presale_has_ended:
|
|
||||||
raise CartError(error_messages['ended'])
|
|
||||||
|
|
||||||
seated = self._is_seated(op.item, op.subevent)
|
|
||||||
if (
|
|
||||||
seated and (
|
|
||||||
not op.seat or (
|
|
||||||
op.seat.blocked and
|
|
||||||
self._sales_channel.identifier not in self.event.settings.seating_allow_blocked_seats_for_channel
|
|
||||||
)
|
|
||||||
)
|
|
||||||
):
|
|
||||||
raise CartError(error_messages['seat_invalid'])
|
|
||||||
elif op.seat and not seated:
|
|
||||||
raise CartError(error_messages['seat_forbidden'])
|
|
||||||
elif op.seat and op.seat.product != op.item:
|
|
||||||
raise CartError(error_messages['seat_invalid'])
|
|
||||||
elif op.seat and op.count > 1:
|
|
||||||
raise CartError('Invalid request: A seat can only be bought once.')
|
raise CartError('Invalid request: A seat can only be bought once.')
|
||||||
|
|
||||||
if op.subevent:
|
if isinstance(op, self.AddOperation):
|
||||||
tlv = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
is_addon = op.addon_to
|
||||||
if tlv:
|
is_bundled = op.addon_to == "FAKE"
|
||||||
term_last = make_aware(datetime.combine(
|
else:
|
||||||
tlv.datetime(op.subevent).date(),
|
is_addon = op.position.addon_to
|
||||||
time(hour=23, minute=59, second=59)
|
is_bundled = op.position.is_bundled
|
||||||
), self.event.timezone)
|
|
||||||
if term_last < time_machine_now(self.real_now_dt):
|
|
||||||
raise CartError(error_messages['payment_ended'])
|
|
||||||
|
|
||||||
if isinstance(op, self.AddOperation):
|
try:
|
||||||
if op.item.category and op.item.category.is_addon and not (op.addon_to and op.addon_to != 'FAKE'):
|
_check_position_constraints(
|
||||||
raise CartError(error_messages['addon_only'])
|
event=self.event,
|
||||||
|
item=op.item,
|
||||||
if op.item.require_bundling and not op.addon_to == 'FAKE':
|
variation=op.variation,
|
||||||
raise CartError(error_messages['bundled_only'])
|
voucher=op.voucher,
|
||||||
|
subevent=op.subevent,
|
||||||
|
seat=op.seat,
|
||||||
|
sales_channel=self._sales_channel,
|
||||||
|
already_in_cart=isinstance(op, self.ExtendOperation),
|
||||||
|
cart_is_expired=isinstance(op, self.ExtendOperation),
|
||||||
|
real_now_dt=self.real_now_dt,
|
||||||
|
item_requires_seat=self._is_seated(op.item, op.subevent),
|
||||||
|
is_addon=is_addon,
|
||||||
|
is_bundled=is_bundled,
|
||||||
|
)
|
||||||
|
# Quota, seat, and voucher availability is checked for in perform_operations
|
||||||
|
# Price changes are checked for in extend_expired_positions
|
||||||
|
except CartPositionError as e:
|
||||||
|
if e.args[0] == error_messages['voucher_required'] and getattr(op, 'voucher_ignored', False):
|
||||||
|
# This is the case where someone clicks +1 on a voucher-only item with a fully redeemed voucher:
|
||||||
|
raise CartPositionError(error_messages['voucher_redeemed'])
|
||||||
|
raise
|
||||||
|
|
||||||
def _get_price(self, item: Item, variation: Optional[ItemVariation],
|
def _get_price(self, item: Item, variation: Optional[ItemVariation],
|
||||||
voucher: Optional[Voucher], custom_price: Optional[Decimal],
|
voucher: Optional[Voucher], custom_price: Optional[Decimal],
|
||||||
@@ -541,7 +632,7 @@ class CartManager:
|
|||||||
else:
|
else:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def extend_expired_positions(self):
|
def _extend_expired_positions(self):
|
||||||
requires_seat = Exists(
|
requires_seat = Exists(
|
||||||
SeatCategoryMapping.objects.filter(
|
SeatCategoryMapping.objects.filter(
|
||||||
Q(product=OuterRef('item'))
|
Q(product=OuterRef('item'))
|
||||||
@@ -604,10 +695,14 @@ class CartManager:
|
|||||||
quotas=quotas, subevent=cp.subevent, seat=cp.seat, listed_price=listed_price,
|
quotas=quotas, subevent=cp.subevent, seat=cp.seat, listed_price=listed_price,
|
||||||
price_after_voucher=price_after_voucher,
|
price_after_voucher=price_after_voucher,
|
||||||
)
|
)
|
||||||
self._check_item_constraints(op)
|
try:
|
||||||
|
self._check_item_constraints(op)
|
||||||
|
except CartPositionError as e:
|
||||||
|
self._operations.append(self.RemoveOperation(position=cp))
|
||||||
|
err = error_messages['positions_removed'] % str(e)
|
||||||
|
|
||||||
if cp.voucher:
|
if cp.voucher:
|
||||||
self._voucher_use_diff[cp.voucher] += 2
|
self._voucher_use_diff[cp.voucher] += 1
|
||||||
|
|
||||||
self._operations.append(op)
|
self._operations.append(op)
|
||||||
return err
|
return err
|
||||||
@@ -797,7 +892,7 @@ class CartManager:
|
|||||||
custom_price_input_is_net=False,
|
custom_price_input_is_net=False,
|
||||||
voucher_ignored=False,
|
voucher_ignored=False,
|
||||||
)
|
)
|
||||||
self._check_item_constraints(bop, operations)
|
self._check_item_constraints(bop)
|
||||||
bundled.append(bop)
|
bundled.append(bop)
|
||||||
|
|
||||||
listed_price = get_listed_price(item, variation, subevent)
|
listed_price = get_listed_price(item, variation, subevent)
|
||||||
@@ -836,7 +931,7 @@ class CartManager:
|
|||||||
custom_price_input_is_net=self.event.settings.display_net_prices,
|
custom_price_input_is_net=self.event.settings.display_net_prices,
|
||||||
voucher_ignored=voucher_ignored,
|
voucher_ignored=voucher_ignored,
|
||||||
)
|
)
|
||||||
self._check_item_constraints(op, operations)
|
self._check_item_constraints(op)
|
||||||
operations.append(op)
|
operations.append(op)
|
||||||
|
|
||||||
self._quota_diff.update(quota_diff)
|
self._quota_diff.update(quota_diff)
|
||||||
@@ -975,7 +1070,7 @@ class CartManager:
|
|||||||
custom_price_input_is_net=self.event.settings.display_net_prices,
|
custom_price_input_is_net=self.event.settings.display_net_prices,
|
||||||
voucher_ignored=False,
|
voucher_ignored=False,
|
||||||
)
|
)
|
||||||
self._check_item_constraints(op, operations)
|
self._check_item_constraints(op)
|
||||||
operations.append(op)
|
operations.append(op)
|
||||||
|
|
||||||
# Check constraints on the add-on combinations
|
# Check constraints on the add-on combinations
|
||||||
@@ -1172,7 +1267,9 @@ class CartManager:
|
|||||||
op.position.delete()
|
op.position.delete()
|
||||||
|
|
||||||
elif isinstance(op, (self.AddOperation, self.ExtendOperation)):
|
elif isinstance(op, (self.AddOperation, self.ExtendOperation)):
|
||||||
# Create a CartPosition for as much items as we can
|
if isinstance(op, self.ExtendOperation) and (op.position.pk in deleted_positions or not op.position.pk):
|
||||||
|
continue # Already deleted in other operation
|
||||||
|
# Create a CartPosition for as many items as we can
|
||||||
requested_count = quota_available_count = voucher_available_count = op.count
|
requested_count = quota_available_count = voucher_available_count = op.count
|
||||||
|
|
||||||
if op.seat:
|
if op.seat:
|
||||||
@@ -1343,6 +1440,8 @@ class CartManager:
|
|||||||
addons.delete()
|
addons.delete()
|
||||||
op.position.delete()
|
op.position.delete()
|
||||||
elif available_count == 1:
|
elif available_count == 1:
|
||||||
|
if op.price_after_voucher != op.position.price_after_voucher:
|
||||||
|
self.price_change_for_extended = True
|
||||||
op.position.expires = self._expiry
|
op.position.expires = self._expiry
|
||||||
op.position.max_extend = self._max_expiry_extend
|
op.position.max_extend = self._max_expiry_extend
|
||||||
op.position.listed_price = op.listed_price
|
op.position.listed_price = op.listed_price
|
||||||
@@ -1429,7 +1528,7 @@ class CartManager:
|
|||||||
self._sales_channel.identifier,
|
self._sales_channel.identifier,
|
||||||
[
|
[
|
||||||
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.line_price_gross,
|
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.line_price_gross,
|
||||||
bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
|
cp.addon_to, cp.is_bundled, cp.listed_price - cp.price_after_voucher)
|
||||||
for cp in positions
|
for cp in positions
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -1444,15 +1543,24 @@ class CartManager:
|
|||||||
|
|
||||||
return diff
|
return diff
|
||||||
|
|
||||||
|
def _remove_parents_if_bundles_are_removed(self):
|
||||||
|
removed_positions = {op.position.pk for op in self._operations if isinstance(op, self.RemoveOperation)}
|
||||||
|
for op in self._operations:
|
||||||
|
if isinstance(op, self.RemoveOperation):
|
||||||
|
if op.position.is_bundled and op.position.addon_to_id not in removed_positions:
|
||||||
|
self._operations.append(self.RemoveOperation(position=op.position.addon_to))
|
||||||
|
removed_positions.add(op.position.addon_to_id)
|
||||||
|
|
||||||
def commit(self):
|
def commit(self):
|
||||||
self._check_presale_dates()
|
self._check_presale_dates()
|
||||||
self._check_max_cart_size()
|
self._check_max_cart_size()
|
||||||
|
|
||||||
err = self._delete_out_of_timeframe()
|
err = self._delete_out_of_timeframe()
|
||||||
err = self.extend_expired_positions() or err
|
err = self._extend_expired_positions() or err
|
||||||
err = err or self._check_min_per_voucher()
|
err = err or self._check_min_per_voucher()
|
||||||
|
|
||||||
self._extend_expiry_of_valid_existing_positions()
|
self._extend_expiry_of_valid_existing_positions()
|
||||||
|
self._remove_parents_if_bundles_are_removed()
|
||||||
err = self._perform_operations() or err
|
err = self._perform_operations() or err
|
||||||
self.recompute_final_prices_and_taxes()
|
self.recompute_final_prices_and_taxes()
|
||||||
|
|
||||||
@@ -1708,7 +1816,12 @@ def extend_cart_reservation(self, event: Event, cart_id: str=None, locale='en',
|
|||||||
try:
|
try:
|
||||||
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
||||||
cm.commit()
|
cm.commit()
|
||||||
return {"success": cm.num_extended_positions, "expiry": cm._expiry, "max_expiry_extend": cm._max_expiry_extend}
|
return {
|
||||||
|
"success": cm.num_extended_positions,
|
||||||
|
"expiry": cm._expiry,
|
||||||
|
"max_expiry_extend": cm._max_expiry_extend,
|
||||||
|
"price_changed": cm.price_change_for_extended,
|
||||||
|
}
|
||||||
except LockTimeoutException:
|
except LockTimeoutException:
|
||||||
self.retry()
|
self.retry()
|
||||||
except (MaxRetriesExceededError, LockTimeoutException):
|
except (MaxRetriesExceededError, LockTimeoutException):
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ class CrossSellingService:
|
|||||||
self.sales_channel,
|
self.sales_channel,
|
||||||
[
|
[
|
||||||
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.line_price_gross,
|
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.line_price_gross,
|
||||||
bool(cp.addon_to), cp.is_bundled,
|
cp.addon_to, cp.is_bundled,
|
||||||
cp.listed_price - cp.price_after_voucher)
|
cp.listed_price - cp.price_after_voucher)
|
||||||
for cp in self.cartpositions
|
for cp in self.cartpositions
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ from urllib.parse import urljoin, urlparse
|
|||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from celery import chain
|
from celery import chain
|
||||||
from celery.exceptions import MaxRetriesExceededError
|
from celery.exceptions import MaxRetriesExceededError
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -764,6 +763,8 @@ def render_mail(template, context, placeholder_mode=SafeFormatter.MODE_RICH_TO_P
|
|||||||
|
|
||||||
|
|
||||||
def replace_images_with_cid_paths(body_html):
|
def replace_images_with_cid_paths(body_html):
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
if body_html:
|
if body_html:
|
||||||
email = BeautifulSoup(body_html, "lxml")
|
email = BeautifulSoup(body_html, "lxml")
|
||||||
cid_images = []
|
cid_images = []
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
|||||||
from pretix.base.payment import GiftCardPayment, PaymentException
|
from pretix.base.payment import GiftCardPayment, PaymentException
|
||||||
from pretix.base.reldate import RelativeDateWrapper
|
from pretix.base.reldate import RelativeDateWrapper
|
||||||
from pretix.base.secrets import assign_ticket_secret
|
from pretix.base.secrets import assign_ticket_secret
|
||||||
from pretix.base.services import tickets
|
from pretix.base.services import cart, tickets
|
||||||
from pretix.base.services.invoices import (
|
from pretix.base.services.invoices import (
|
||||||
generate_cancellation, generate_invoice, invoice_qualified,
|
generate_cancellation, generate_invoice, invoice_qualified,
|
||||||
invoice_transmission_separately, order_invoice_transmission_separately,
|
invoice_transmission_separately, order_invoice_transmission_separately,
|
||||||
@@ -130,6 +130,9 @@ class OrderError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
error_messages = {
|
error_messages = {
|
||||||
|
'positions_removed': gettext_lazy(
|
||||||
|
'Some products can no longer be purchased and have been removed from your cart for the following reason: %s'
|
||||||
|
),
|
||||||
'unavailable': gettext_lazy(
|
'unavailable': gettext_lazy(
|
||||||
'Some of the products you selected were no longer available. '
|
'Some of the products you selected were no longer available. '
|
||||||
'Please see below for details.'
|
'Please see below for details.'
|
||||||
@@ -182,14 +185,6 @@ error_messages = {
|
|||||||
'The voucher code used for one of the items in your cart is not valid for this item. We removed this item from your cart.'
|
'The voucher code used for one of the items in your cart is not valid for this item. We removed this item from your cart.'
|
||||||
),
|
),
|
||||||
'voucher_required': gettext_lazy('You need a valid voucher code to order one of the products.'),
|
'voucher_required': gettext_lazy('You need a valid voucher code to order one of the products.'),
|
||||||
'some_subevent_not_started': gettext_lazy(
|
|
||||||
'The booking period for one of the events in your cart has not yet started. The '
|
|
||||||
'affected positions have been removed from your cart.'
|
|
||||||
),
|
|
||||||
'some_subevent_ended': gettext_lazy(
|
|
||||||
'The booking period for one of the events in your cart has ended. The affected '
|
|
||||||
'positions have been removed from your cart.'
|
|
||||||
),
|
|
||||||
'seat_invalid': gettext_lazy('One of the seats in your order was invalid, we removed the position from your cart.'),
|
'seat_invalid': gettext_lazy('One of the seats in your order was invalid, we removed the position from your cart.'),
|
||||||
'seat_unavailable': gettext_lazy('One of the seats in your order has been taken in the meantime, we removed the position from your cart.'),
|
'seat_unavailable': gettext_lazy('One of the seats in your order has been taken in the meantime, we removed the position from your cart.'),
|
||||||
'country_blocked': gettext_lazy('One of the selected products is not available in the selected country.'),
|
'country_blocked': gettext_lazy('One of the selected products is not available in the selected country.'),
|
||||||
@@ -744,12 +739,37 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
|||||||
deleted_positions.add(cp.pk)
|
deleted_positions.add(cp.pk)
|
||||||
cp.delete()
|
cp.delete()
|
||||||
|
|
||||||
sorted_positions = sorted(positions, key=lambda c: (-int(c.is_bundled), c.pk))
|
sorted_positions = list(sorted(positions, key=lambda c: (-int(c.is_bundled), c.pk)))
|
||||||
|
|
||||||
for cp in sorted_positions:
|
for cp in sorted_positions:
|
||||||
cp._cached_quotas = list(cp.quotas)
|
cp._cached_quotas = list(cp.quotas)
|
||||||
|
|
||||||
|
for cp in sorted_positions:
|
||||||
|
try:
|
||||||
|
cart._check_position_constraints(
|
||||||
|
event=event,
|
||||||
|
item=cp.item,
|
||||||
|
variation=cp.variation,
|
||||||
|
voucher=cp.voucher,
|
||||||
|
subevent=cp.subevent,
|
||||||
|
seat=cp.seat,
|
||||||
|
sales_channel=sales_channel,
|
||||||
|
already_in_cart=True,
|
||||||
|
cart_is_expired=cp.expires < now_dt,
|
||||||
|
real_now_dt=now_dt,
|
||||||
|
item_requires_seat=cp.requires_seat,
|
||||||
|
is_addon=bool(cp.addon_to_id),
|
||||||
|
is_bundled=bool(cp.addon_to_id) and cp.is_bundled,
|
||||||
|
)
|
||||||
|
# Quota, seat, and voucher availability is checked for below
|
||||||
|
# Prices are checked for below
|
||||||
|
# Memberships are checked in _create_order
|
||||||
|
except cart.CartPositionError as e:
|
||||||
|
err = error_messages['positions_removed'] % str(e)
|
||||||
|
delete(cp)
|
||||||
|
|
||||||
# Create locks
|
# Create locks
|
||||||
|
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions] # eliminate deleted
|
||||||
if any(cp.expires < now() + timedelta(seconds=LOCK_TRUST_WINDOW) for cp in sorted_positions):
|
if any(cp.expires < now() + timedelta(seconds=LOCK_TRUST_WINDOW) for cp in sorted_positions):
|
||||||
# No need to perform any locking if the cart positions still guarantee everything long enough.
|
# No need to perform any locking if the cart positions still guarantee everything long enough.
|
||||||
full_lock_required = any(
|
full_lock_required = any(
|
||||||
@@ -774,15 +794,12 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
|||||||
|
|
||||||
# Check availability
|
# Check availability
|
||||||
for i, cp in enumerate(sorted_positions):
|
for i, cp in enumerate(sorted_positions):
|
||||||
if cp.pk in deleted_positions:
|
if cp.pk in deleted_positions or not cp.pk:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not cp.item.is_available() or (cp.variation and not cp.variation.is_available()):
|
|
||||||
err = err or error_messages['unavailable']
|
|
||||||
delete(cp)
|
|
||||||
continue
|
|
||||||
quotas = cp._cached_quotas
|
quotas = cp._cached_quotas
|
||||||
|
|
||||||
|
# Product per order limits
|
||||||
products_seen[cp.item] += 1
|
products_seen[cp.item] += 1
|
||||||
if cp.item.max_per_order and products_seen[cp.item] > cp.item.max_per_order:
|
if cp.item.max_per_order and products_seen[cp.item] > cp.item.max_per_order:
|
||||||
err = error_messages['max_items_per_product'] % {
|
err = error_messages['max_items_per_product'] % {
|
||||||
@@ -792,6 +809,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
|||||||
delete(cp)
|
delete(cp)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Voucher availability
|
||||||
if cp.voucher:
|
if cp.voucher:
|
||||||
v_usages[cp.voucher] += 1
|
v_usages[cp.voucher] += 1
|
||||||
if cp.voucher not in v_avail:
|
if cp.voucher not in v_avail:
|
||||||
@@ -806,48 +824,14 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
|||||||
delete(cp)
|
delete(cp)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if cp.subevent and cp.subevent.presale_start and time_machine_now_dt < cp.subevent.presale_start:
|
# Check duplicate seats in order
|
||||||
err = err or error_messages['some_subevent_not_started']
|
if cp.seat in seats_seen:
|
||||||
delete(cp)
|
|
||||||
break
|
|
||||||
|
|
||||||
if cp.subevent:
|
|
||||||
tlv = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
|
||||||
if tlv:
|
|
||||||
term_last = make_aware(datetime.combine(
|
|
||||||
tlv.datetime(cp.subevent).date(),
|
|
||||||
time(hour=23, minute=59, second=59)
|
|
||||||
), event.timezone)
|
|
||||||
if term_last < time_machine_now_dt:
|
|
||||||
err = err or error_messages['some_subevent_ended']
|
|
||||||
delete(cp)
|
|
||||||
break
|
|
||||||
|
|
||||||
if cp.subevent and cp.subevent.presale_has_ended:
|
|
||||||
err = err or error_messages['some_subevent_ended']
|
|
||||||
delete(cp)
|
|
||||||
break
|
|
||||||
|
|
||||||
if (cp.requires_seat and not cp.seat) or (cp.seat and not cp.requires_seat) or (cp.seat and cp.seat.product != cp.item) or cp.seat in seats_seen:
|
|
||||||
err = err or error_messages['seat_invalid']
|
err = err or error_messages['seat_invalid']
|
||||||
delete(cp)
|
delete(cp)
|
||||||
break
|
break
|
||||||
|
|
||||||
if cp.seat:
|
if cp.seat:
|
||||||
seats_seen.add(cp.seat)
|
seats_seen.add(cp.seat)
|
||||||
|
|
||||||
if cp.item.require_voucher and cp.voucher is None and not cp.is_bundled:
|
|
||||||
delete(cp)
|
|
||||||
err = err or error_messages['voucher_required']
|
|
||||||
break
|
|
||||||
|
|
||||||
if (cp.item.hide_without_voucher or (cp.variation and cp.variation.hide_without_voucher)) and (
|
|
||||||
cp.voucher is None or not cp.voucher.show_hidden_items or not cp.voucher.applies_to(cp.item, cp.variation)
|
|
||||||
) and not cp.is_bundled:
|
|
||||||
delete(cp)
|
|
||||||
err = error_messages['voucher_required']
|
|
||||||
break
|
|
||||||
|
|
||||||
if cp.seat:
|
|
||||||
# Unlike quotas (which we blindly trust as long as the position is not expired), we check seats every
|
# Unlike quotas (which we blindly trust as long as the position is not expired), we check seats every
|
||||||
# time, since we absolutely can not overbook a seat.
|
# time, since we absolutely can not overbook a seat.
|
||||||
if not cp.seat.is_available(ignore_cart=cp, ignore_voucher_id=cp.voucher_id, sales_channel=sales_channel.identifier):
|
if not cp.seat.is_available(ignore_cart=cp, ignore_voucher_id=cp.voucher_id, sales_channel=sales_channel.identifier):
|
||||||
@@ -855,34 +839,13 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
|||||||
delete(cp)
|
delete(cp)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if cp.expires >= now_dt and not cp.voucher:
|
# Check useful quota configuration
|
||||||
# Other checks are not necessary
|
|
||||||
continue
|
|
||||||
|
|
||||||
if len(quotas) == 0:
|
if len(quotas) == 0:
|
||||||
err = err or error_messages['unavailable']
|
err = err or error_messages['unavailable']
|
||||||
delete(cp)
|
delete(cp)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if cp.subevent and cp.item.pk in cp.subevent.item_overrides and not cp.subevent.item_overrides[cp.item.pk].is_available(time_machine_now_dt):
|
|
||||||
err = err or error_messages['unavailable']
|
|
||||||
delete(cp)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if cp.subevent and cp.variation and cp.variation.pk in cp.subevent.var_overrides and \
|
|
||||||
not cp.subevent.var_overrides[cp.variation.pk].is_available(time_machine_now_dt):
|
|
||||||
err = err or error_messages['unavailable']
|
|
||||||
delete(cp)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if cp.voucher:
|
|
||||||
if cp.voucher.valid_until and cp.voucher.valid_until < time_machine_now_dt:
|
|
||||||
err = err or error_messages['voucher_expired']
|
|
||||||
delete(cp)
|
|
||||||
continue
|
|
||||||
|
|
||||||
quota_ok = True
|
quota_ok = True
|
||||||
|
|
||||||
ignore_all_quotas = cp.expires >= now_dt or (
|
ignore_all_quotas = cp.expires >= now_dt or (
|
||||||
cp.voucher and (
|
cp.voucher and (
|
||||||
cp.voucher.allow_ignore_quota or (cp.voucher.block_quota and cp.voucher.quota is None)
|
cp.voucher.allow_ignore_quota or (cp.voucher.block_quota and cp.voucher.quota is None)
|
||||||
@@ -914,7 +877,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
|||||||
})
|
})
|
||||||
|
|
||||||
# Check prices
|
# Check prices
|
||||||
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions]
|
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions] # eliminate deleted
|
||||||
old_total = sum(cp.price for cp in sorted_positions)
|
old_total = sum(cp.price for cp in sorted_positions)
|
||||||
for i, cp in enumerate(sorted_positions):
|
for i, cp in enumerate(sorted_positions):
|
||||||
if cp.listed_price is None:
|
if cp.listed_price is None:
|
||||||
@@ -945,13 +908,13 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
|||||||
delete(cp)
|
delete(cp)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions]
|
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions] # eliminate deleted
|
||||||
discount_results = apply_discounts(
|
discount_results = apply_discounts(
|
||||||
event,
|
event,
|
||||||
sales_channel.identifier,
|
sales_channel.identifier,
|
||||||
[
|
[
|
||||||
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.line_price_gross,
|
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.line_price_gross,
|
||||||
bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
|
cp.addon_to, cp.is_bundled, cp.listed_price - cp.price_after_voucher)
|
||||||
for cp in sorted_positions
|
for cp in sorted_positions
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -1667,7 +1630,7 @@ class OrderChangeManager:
|
|||||||
MembershipOperation = namedtuple('MembershipOperation', ('position', 'membership'))
|
MembershipOperation = namedtuple('MembershipOperation', ('position', 'membership'))
|
||||||
CancelOperation = namedtuple('CancelOperation', ('position', 'price_diff'))
|
CancelOperation = namedtuple('CancelOperation', ('position', 'price_diff'))
|
||||||
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent', 'seat', 'membership',
|
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent', 'seat', 'membership',
|
||||||
'valid_from', 'valid_until', 'is_bundled'))
|
'valid_from', 'valid_until', 'is_bundled', 'result'))
|
||||||
SplitOperation = namedtuple('SplitOperation', ('position',))
|
SplitOperation = namedtuple('SplitOperation', ('position',))
|
||||||
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value', 'price_diff'))
|
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value', 'price_diff'))
|
||||||
AddFeeOperation = namedtuple('AddFeeOperation', ('fee', 'price_diff'))
|
AddFeeOperation = namedtuple('AddFeeOperation', ('fee', 'price_diff'))
|
||||||
@@ -1679,6 +1642,18 @@ class OrderChangeManager:
|
|||||||
AddBlockOperation = namedtuple('AddBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
|
AddBlockOperation = namedtuple('AddBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
|
||||||
RemoveBlockOperation = namedtuple('RemoveBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
|
RemoveBlockOperation = namedtuple('RemoveBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
|
||||||
|
|
||||||
|
class AddPositionResult:
|
||||||
|
_position: Optional[OrderPosition]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._position = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def position(self) -> OrderPosition:
|
||||||
|
if self._position is None:
|
||||||
|
raise RuntimeError("Order position has not been created yet. Call commit() first on OrderChangeManager.")
|
||||||
|
return self._position
|
||||||
|
|
||||||
def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True, allow_blocked_seats=False):
|
def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True, allow_blocked_seats=False):
|
||||||
self.order = order
|
self.order = order
|
||||||
self.user = user
|
self.user = user
|
||||||
@@ -1883,7 +1858,7 @@ class OrderChangeManager:
|
|||||||
|
|
||||||
def add_position(self, item: Item, variation: ItemVariation, price: Decimal, addon_to: OrderPosition = None,
|
def add_position(self, item: Item, variation: ItemVariation, price: Decimal, addon_to: OrderPosition = None,
|
||||||
subevent: SubEvent = None, seat: Seat = None, membership: Membership = None,
|
subevent: SubEvent = None, seat: Seat = None, membership: Membership = None,
|
||||||
valid_from: datetime = None, valid_until: datetime = None):
|
valid_from: datetime = None, valid_until: datetime = None) -> 'OrderChangeManager.AddPositionResult':
|
||||||
if isinstance(seat, str):
|
if isinstance(seat, str):
|
||||||
if not seat:
|
if not seat:
|
||||||
seat = None
|
seat = None
|
||||||
@@ -1942,8 +1917,11 @@ class OrderChangeManager:
|
|||||||
self._quotadiff.update(new_quotas)
|
self._quotadiff.update(new_quotas)
|
||||||
if seat:
|
if seat:
|
||||||
self._seatdiff.update([seat])
|
self._seatdiff.update([seat])
|
||||||
|
|
||||||
|
result = self.AddPositionResult()
|
||||||
self._operations.append(self.AddOperation(item, variation, price, addon_to, subevent, seat, membership,
|
self._operations.append(self.AddOperation(item, variation, price, addon_to, subevent, seat, membership,
|
||||||
valid_from, valid_until, is_bundled))
|
valid_from, valid_until, is_bundled, result))
|
||||||
|
return result
|
||||||
|
|
||||||
def split(self, position: OrderPosition):
|
def split(self, position: OrderPosition):
|
||||||
if self.order.event.settings.invoice_include_free or position.price != Decimal('0.00'):
|
if self.order.event.settings.invoice_include_free or position.price != Decimal('0.00'):
|
||||||
@@ -2562,6 +2540,7 @@ class OrderChangeManager:
|
|||||||
'valid_from': op.valid_from.isoformat() if op.valid_from else None,
|
'valid_from': op.valid_from.isoformat() if op.valid_from else None,
|
||||||
'valid_until': op.valid_until.isoformat() if op.valid_until else None,
|
'valid_until': op.valid_until.isoformat() if op.valid_until else None,
|
||||||
})
|
})
|
||||||
|
op.result._position = pos
|
||||||
elif isinstance(op, self.SplitOperation):
|
elif isinstance(op, self.SplitOperation):
|
||||||
position = position_cache.setdefault(op.position.pk, op.position)
|
position = position_cache.setdefault(op.position.pk, op.position)
|
||||||
split_positions.append(position)
|
split_positions.append(position)
|
||||||
|
|||||||
@@ -801,7 +801,11 @@ def get_sample_context(event, context_parameters, rich=True):
|
|||||||
sample = v.render_sample(event)
|
sample = v.render_sample(event)
|
||||||
if isinstance(sample, PlainHtmlAlternativeString):
|
if isinstance(sample, PlainHtmlAlternativeString):
|
||||||
context_dict[k] = PlainHtmlAlternativeString(
|
context_dict[k] = PlainHtmlAlternativeString(
|
||||||
sample.plain,
|
'<{el} class="placeholder" title="{title}">{plain}</{el}>'.format(
|
||||||
|
el='span',
|
||||||
|
title=lbl,
|
||||||
|
plain=escape(sample.plain),
|
||||||
|
),
|
||||||
'<{el} class="placeholder placeholder-html" title="{title}">{html}</{el}>'.format(
|
'<{el} class="placeholder placeholder-html" title="{title}">{html}</{el}>'.format(
|
||||||
el='div' if sample.is_block else 'span',
|
el='div' if sample.is_block else 'span',
|
||||||
title=lbl,
|
title=lbl,
|
||||||
|
|||||||
@@ -174,7 +174,9 @@ def apply_discounts(event: Event, sales_channel: Union[str, SalesChannel],
|
|||||||
|
|
||||||
:param event: Event the cart belongs to
|
:param event: Event the cart belongs to
|
||||||
:param sales_channel: Sales channel the cart was created with
|
:param sales_channel: Sales channel the cart was created with
|
||||||
:param positions: Tuple of the form ``(item_id, subevent_id, subevent_date_from, line_price_gross, is_addon_to, is_bundled, voucher_discount)``
|
:param positions: Tuple of the form ``(item_id, subevent_id, subevent_date_from, line_price_gross, addon_to_id, is_bundled, voucher_discount)``
|
||||||
|
``addon_to_id`` does not have to be the proper ID, any identifier is okay, even ``True``/``False`` are accepted, but
|
||||||
|
a better result may be given if addons to the same main product have the same distinct value.
|
||||||
:param collect_potential_discounts: If a `defaultdict(list)` is supplied, all discounts that could be applied to the cart
|
:param collect_potential_discounts: If a `defaultdict(list)` is supplied, all discounts that could be applied to the cart
|
||||||
based on the "consumed" items, but lack matching "benefitting" items will be collected therein.
|
based on the "consumed" items, but lack matching "benefitting" items will be collected therein.
|
||||||
The dict will contain a mapping from index in the `positions` list of the item that could be consumed, to a list
|
The dict will contain a mapping from index in the `positions` list of the item that could be consumed, to a list
|
||||||
@@ -196,9 +198,9 @@ def apply_discounts(event: Event, sales_channel: Union[str, SalesChannel],
|
|||||||
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
|
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
|
||||||
for discount in discount_qs:
|
for discount in discount_qs:
|
||||||
result = discount.apply({
|
result = discount.apply({
|
||||||
idx: PositionInfo(item_id, subevent_id, subevent_date_from, line_price_gross, is_addon_to, voucher_discount)
|
idx: PositionInfo(item_id, subevent_id, subevent_date_from, line_price_gross, addon_to, voucher_discount)
|
||||||
for
|
for
|
||||||
idx, (item_id, subevent_id, subevent_date_from, line_price_gross, is_addon_to, is_bundled, voucher_discount)
|
idx, (item_id, subevent_id, subevent_date_from, line_price_gross, addon_to, is_bundled, voucher_discount)
|
||||||
in enumerate(positions)
|
in enumerate(positions)
|
||||||
if not is_bundled and idx not in new_prices
|
if not is_bundled and idx not in new_prices
|
||||||
}, collect_potential_discounts)
|
}, collect_potential_discounts)
|
||||||
|
|||||||
@@ -180,6 +180,19 @@ DEFAULTS = {
|
|||||||
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_settings-customer_accounts'}),
|
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_settings-customer_accounts'}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
'customer_accounts_require_login_for_order_access': {
|
||||||
|
'default': 'False',
|
||||||
|
'type': bool,
|
||||||
|
'form_class': forms.BooleanField,
|
||||||
|
'serializer_class': serializers.BooleanField,
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_("Require login to access order confirmation pages"),
|
||||||
|
help_text=_("If enabled, users who were logged in at the time of purchase must also log in to access their order information. "
|
||||||
|
"If a customer account is created while placing an order, the restriction only becomes active after the customer "
|
||||||
|
"account is activated."),
|
||||||
|
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_settings-customer_accounts'}),
|
||||||
|
)
|
||||||
|
},
|
||||||
'customer_accounts_link_by_email': {
|
'customer_accounts_link_by_email': {
|
||||||
'default': 'False',
|
'default': 'False',
|
||||||
'type': bool,
|
'type': bool,
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#
|
||||||
|
# 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 <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django import template
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.utils.timezone import get_current_timezone
|
||||||
|
|
||||||
|
from pretix.base.i18n import LazyExpiresDate
|
||||||
|
from pretix.helpers.templatetags.date_fast import date_fast
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def html_time(value: datetime, dt_format: str = "SHORT_DATE_FORMAT", **kwargs):
|
||||||
|
"""
|
||||||
|
Building a <time datetime='{html-datetime}'>{human-readable datetime}</time> html string,
|
||||||
|
where the html-datetime as well as the human-readable datetime can be set
|
||||||
|
to a value from django's FORMAT_SETTINGS or "format_expires".
|
||||||
|
|
||||||
|
If attr_fmt isn’t provided, it will be set to isoformat.
|
||||||
|
|
||||||
|
Usage example:
|
||||||
|
{% html_time event_start "SHORT_DATETIME_FORMAT" %}
|
||||||
|
or
|
||||||
|
{% html_time event_start "TIME_FORMAT" attr_fmt="H:i" %}
|
||||||
|
"""
|
||||||
|
if value in (None, ''):
|
||||||
|
return ''
|
||||||
|
value = value.astimezone(get_current_timezone())
|
||||||
|
attr_fmt = kwargs["attr_fmt"] if kwargs else None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not attr_fmt:
|
||||||
|
date_html = value.isoformat()
|
||||||
|
else:
|
||||||
|
date_html = date_fast(value, attr_fmt)
|
||||||
|
|
||||||
|
if dt_format == "format_expires":
|
||||||
|
date_human = LazyExpiresDate(value)
|
||||||
|
else:
|
||||||
|
date_human = date_fast(value, dt_format)
|
||||||
|
return format_html("<time datetime='{}'>{}</time>", date_html, date_human)
|
||||||
|
except AttributeError:
|
||||||
|
return ''
|
||||||
@@ -26,7 +26,8 @@ from babel.numbers import format_currency
|
|||||||
from django import template
|
from django import template
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.template.defaultfilters import floatformat
|
from django.template.defaultfilters import floatformat
|
||||||
from django.utils import translation
|
|
||||||
|
from pretix.base.i18n import get_babel_locale
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
@@ -59,13 +60,10 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
|
|||||||
if hide_currency:
|
if hide_currency:
|
||||||
return floatformat(value, f"{places}g")
|
return floatformat(value, f"{places}g")
|
||||||
|
|
||||||
locale_parts = translation.get_language().split("-", 1)
|
try:
|
||||||
locale = locale_parts[0]
|
locale = Locale(get_babel_locale())
|
||||||
if len(locale_parts) > 1 and len(locale_parts[1]) == 2:
|
except UnknownLocaleError:
|
||||||
try:
|
locale = "en"
|
||||||
locale = Locale(locale_parts[0], locale_parts[1].upper())
|
|
||||||
except UnknownLocaleError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return format_currency(value, arg, locale=locale)
|
return format_currency(value, arg, locale=locale)
|
||||||
|
|||||||
@@ -32,13 +32,14 @@
|
|||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# 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.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
|
import html
|
||||||
import re
|
import re
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
import bleach
|
import bleach
|
||||||
import markdown
|
import markdown
|
||||||
from bleach import DEFAULT_CALLBACKS
|
from bleach import DEFAULT_CALLBACKS, html5lib_shim
|
||||||
from bleach.linkifier import build_email_re, build_url_re
|
from bleach.linkifier import build_email_re
|
||||||
from django import template
|
from django import template
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import signing
|
from django.core import signing
|
||||||
@@ -124,6 +125,23 @@ ALLOWED_ATTRIBUTES = {
|
|||||||
|
|
||||||
ALLOWED_PROTOCOLS = {'http', 'https', 'mailto', 'tel'}
|
ALLOWED_PROTOCOLS = {'http', 'https', 'mailto', 'tel'}
|
||||||
|
|
||||||
|
|
||||||
|
def build_url_re(tlds=tld_set, protocols=html5lib_shim.allowed_protocols):
|
||||||
|
# Differs from bleach regex by allowing { and } in URL to allow placeholders in URL parameters
|
||||||
|
return re.compile(
|
||||||
|
r"""\(* # Match any opening parentheses.
|
||||||
|
\b(?<![@.])(?:(?:{0}):/{{0,3}}(?:(?:\w+:)?\w+@)?)? # http://
|
||||||
|
([\w-]+\.)+(?:{1})(?:\:[0-9]+)?(?!\.\w)\b # xx.yy.tld(:##)?
|
||||||
|
(?:[/?][^\s\|\\\^`<>"]*)?
|
||||||
|
# /path/zz (excluding "unsafe" chars from RFC 3986,
|
||||||
|
# except for # and ~, which happen in practice)
|
||||||
|
""".format(
|
||||||
|
"|".join(sorted(protocols)), "|".join(sorted(tlds))
|
||||||
|
),
|
||||||
|
re.IGNORECASE | re.VERBOSE | re.UNICODE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
URL_RE = SimpleLazyObject(lambda: build_url_re(tlds=sorted(tld_set, key=len, reverse=True)))
|
URL_RE = SimpleLazyObject(lambda: build_url_re(tlds=sorted(tld_set, key=len, reverse=True)))
|
||||||
|
|
||||||
EMAIL_RE = SimpleLazyObject(lambda: build_email_re(tlds=sorted(tld_set, key=len, reverse=True)))
|
EMAIL_RE = SimpleLazyObject(lambda: build_email_re(tlds=sorted(tld_set, key=len, reverse=True)))
|
||||||
@@ -333,8 +351,14 @@ def markdown_compile_email(source, allowed_tags=None, allowed_attributes=ALLOWED
|
|||||||
# This is a workaround to fix placeholders in URL targets
|
# This is a workaround to fix placeholders in URL targets
|
||||||
def context_callback(attrs, new=False):
|
def context_callback(attrs, new=False):
|
||||||
if (None, "href") in attrs and "{" in attrs[None, "href"]:
|
if (None, "href") in attrs and "{" in attrs[None, "href"]:
|
||||||
# Do not use MODE_RICH_TO_HTML to avoid recursive linkification
|
# Do not use MODE_RICH_TO_HTML to avoid recursive linkification.
|
||||||
attrs[None, "href"] = escape(format_map(attrs[None, "href"], context=context, mode=SafeFormatter.MODE_RICH_TO_PLAIN))
|
# We want to esacpe the end result, however, we need to unescape the input to prevent & being turned
|
||||||
|
# to &amp; because the input is already escaped by the markdown parser.
|
||||||
|
attrs[None, "href"] = escape(format_map(
|
||||||
|
html.unescape(attrs[None, "href"]),
|
||||||
|
context=context,
|
||||||
|
mode=SafeFormatter.MODE_RICH_TO_PLAIN
|
||||||
|
))
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
context_callbacks.append(context_callback)
|
context_callbacks.append(context_callback)
|
||||||
|
|||||||
@@ -93,7 +93,9 @@ def timeline_for_event(event, subevent=None):
|
|||||||
description=format_lazy(
|
description=format_lazy(
|
||||||
'{} ({})',
|
'{} ({})',
|
||||||
pgettext_lazy('timeline', 'End of ticket sales'),
|
pgettext_lazy('timeline', 'End of ticket sales'),
|
||||||
pgettext_lazy('timeline', 'automatically because the event is over and no end of presale has been configured') if not ev.presale_end else ""
|
pgettext_lazy('timeline', 'automatically because the event is over and no end of presale has been configured')
|
||||||
|
) if not ev.presale_end else (
|
||||||
|
pgettext_lazy('timeline', 'End of ticket sales')
|
||||||
),
|
),
|
||||||
edit_url=ev_edit_url + '#id_presale_end_0'
|
edit_url=ev_edit_url + '#id_presale_end_0'
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -36,9 +36,8 @@ class DownloadView(TemplateView):
|
|||||||
def object(self) -> CachedFile:
|
def object(self) -> CachedFile:
|
||||||
try:
|
try:
|
||||||
o = get_object_or_404(CachedFile, id=self.kwargs['id'], web_download=True)
|
o = get_object_or_404(CachedFile, id=self.kwargs['id'], web_download=True)
|
||||||
if o.session_key:
|
if not o.allowed_for_session(self.request):
|
||||||
if o.session_key != self.request.session.session_key:
|
raise Http404()
|
||||||
raise Http404()
|
|
||||||
return o
|
return o
|
||||||
except (ValueError, ValidationError): # Invalid URLs
|
except (ValueError, ValidationError): # Invalid URLs
|
||||||
raise Http404()
|
raise Http404()
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ from pretix.base.settings import (
|
|||||||
|
|
||||||
VAT_ID_LABELS = {
|
VAT_ID_LABELS = {
|
||||||
# VAT ID is a EU concept and Switzerland has a distinct, but differently-named concept
|
# VAT ID is a EU concept and Switzerland has a distinct, but differently-named concept
|
||||||
"CH": pgettext_lazy("tax_id_swiss", "UID"), # Translators: Only translate to French (IDE) and Italien (IDI), otherwise keep the same
|
# Translators: Only translate to French (IDE) and Italien (IDI), otherwise keep the same
|
||||||
|
"CH": pgettext_lazy("tax_id_swiss", "UID"),
|
||||||
|
|
||||||
# Awareness around VAT IDs differes by EU country. For example, in Germany the VAT ID is assigned
|
# Awareness around VAT IDs differes by EU country. For example, in Germany the VAT ID is assigned
|
||||||
# separately to each company and only used in cross-country transactions. Therefore, it makes sense
|
# separately to each company and only used in cross-country transactions. Therefore, it makes sense
|
||||||
@@ -46,10 +47,15 @@ VAT_ID_LABELS = {
|
|||||||
# In contrast, in Italy the EU-compatible VAT ID is not separately assigned, but is just "IT" + the national tax
|
# In contrast, in Italy the EU-compatible VAT ID is not separately assigned, but is just "IT" + the national tax
|
||||||
# number (Partita IVA) and also used on domestic transactions. So someone who never purchased something international
|
# number (Partita IVA) and also used on domestic transactions. So someone who never purchased something international
|
||||||
# for their company, might still know the value, if we call it the right way and not just "VAT ID".
|
# for their company, might still know the value, if we call it the right way and not just "VAT ID".
|
||||||
"IT": pgettext_lazy("tax_id_italy", "VAT ID / P.IVA"), # Translators: Translate to only "P.IVA" in Italian, keep second part as-is in other languages
|
|
||||||
"GR": pgettext_lazy("tax_id_greece", "VAT ID / TIN"), # Translators: Translate to only "ΑΦΜ" in Greek
|
# Translators: Translate to only "P.IVA" in Italian, keep second part as-is in other languages
|
||||||
"ES": pgettext_lazy("tax_id_spain", "VAT ID / NIF"), # Translators: Translate to only "NIF" in Spanish
|
"IT": pgettext_lazy("tax_id_italy", "VAT ID / P.IVA"),
|
||||||
"PT": pgettext_lazy("tax_id_portugal", "VAT ID / NIF"), # Translators: Translate to only "NIF" in Portuguese
|
# Translators: Translate to only "ΑΦΜ" in Greek
|
||||||
|
"GR": pgettext_lazy("tax_id_greece", "VAT ID / TIN"),
|
||||||
|
# Translators: Translate to only "NIF" in Spanish
|
||||||
|
"ES": pgettext_lazy("tax_id_spain", "VAT ID / NIF"),
|
||||||
|
# Translators: Translate to only "NIF" in Portuguese
|
||||||
|
"PT": pgettext_lazy("tax_id_portugal", "VAT ID / NIF"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
|
|||||||
from django.db.models import Prefetch, Q, prefetch_related_objects
|
from django.db.models import Prefetch, Q, prefetch_related_objects
|
||||||
from django.forms import formset_factory, inlineformset_factory
|
from django.forms import formset_factory, inlineformset_factory
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property, lazy
|
||||||
from django.utils.html import escape, format_html
|
from django.utils.html import escape, format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.timezone import get_current_timezone_name
|
from django.utils.timezone import get_current_timezone_name
|
||||||
@@ -53,7 +53,7 @@ from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
|
|||||||
from django_countries.fields import LazyTypedChoiceField
|
from django_countries.fields import LazyTypedChoiceField
|
||||||
from django_scopes.forms import SafeModelMultipleChoiceField
|
from django_scopes.forms import SafeModelMultipleChoiceField
|
||||||
from i18nfield.forms import (
|
from i18nfield.forms import (
|
||||||
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextInput,
|
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput,
|
||||||
)
|
)
|
||||||
from pytz import common_timezones
|
from pytz import common_timezones
|
||||||
|
|
||||||
@@ -207,6 +207,7 @@ class EventWizardBasicsForm(I18nModelForm):
|
|||||||
'Sample Conference Center\nHeidelberg, Germany'
|
'Sample Conference Center\nHeidelberg, Germany'
|
||||||
)
|
)
|
||||||
self.fields['slug'].widget.prefix = build_absolute_uri(self.organizer, 'presale:organizer.index')
|
self.fields['slug'].widget.prefix = build_absolute_uri(self.organizer, 'presale:organizer.index')
|
||||||
|
self.fields['tax_rate']._required = True # Do not render as optional because it is conditionally required
|
||||||
if self.has_subevents:
|
if self.has_subevents:
|
||||||
del self.fields['presale_start']
|
del self.fields['presale_start']
|
||||||
del self.fields['presale_end']
|
del self.fields['presale_end']
|
||||||
@@ -1310,9 +1311,17 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
|
|||||||
mail_text_order_invoice = I18nFormField(
|
mail_text_order_invoice = I18nFormField(
|
||||||
label=_("Text"),
|
label=_("Text"),
|
||||||
required=False,
|
required=False,
|
||||||
widget=I18nMarkdownTextarea,
|
widget=I18nTextarea, # no Markdown supported
|
||||||
help_text=_("This will only be used if the invoice is sent to a different email address or at a different time "
|
help_text=lazy(
|
||||||
"than the order confirmation."),
|
lambda: str(_(
|
||||||
|
"This will only be used if the invoice is sent to a different email address or at a different time "
|
||||||
|
"than the order confirmation."
|
||||||
|
)) + " " + str(_(
|
||||||
|
"Formatting is not supported, as some accounting departments process mail automatically and do not "
|
||||||
|
"handle formatted emails properly."
|
||||||
|
)),
|
||||||
|
str
|
||||||
|
)()
|
||||||
)
|
)
|
||||||
mail_subject_download_reminder = I18nFormField(
|
mail_subject_download_reminder = I18nFormField(
|
||||||
label=_("Subject sent to order contact address"),
|
label=_("Subject sent to order contact address"),
|
||||||
@@ -1480,6 +1489,9 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
|
|||||||
'mail_subject_resend_all_links': ['event', 'orders'],
|
'mail_subject_resend_all_links': ['event', 'orders'],
|
||||||
'mail_attach_ical_description': ['event', 'event_or_subevent'],
|
'mail_attach_ical_description': ['event', 'event_or_subevent'],
|
||||||
}
|
}
|
||||||
|
plain_rendering = {
|
||||||
|
'mail_text_order_invoice',
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.event = event = kwargs.get('obj')
|
self.event = event = kwargs.get('obj')
|
||||||
@@ -1498,7 +1510,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
|
|||||||
self.event.meta_values_cached = self.event.meta_values.select_related('property').all()
|
self.event.meta_values_cached = self.event.meta_values.select_related('property').all()
|
||||||
|
|
||||||
for k, v in self.base_context.items():
|
for k, v in self.base_context.items():
|
||||||
self._set_field_placeholders(k, v, rich=k.startswith('mail_text_'))
|
self._set_field_placeholders(k, v, rich=k.startswith('mail_text_') and k not in self.plain_rendering)
|
||||||
|
|
||||||
for k, v in list(self.fields.items()):
|
for k, v in list(self.fields.items()):
|
||||||
if k.endswith('_attendee') and not event.settings.attendee_emails_asked:
|
if k.endswith('_attendee') and not event.settings.attendee_emails_asked:
|
||||||
@@ -1958,6 +1970,13 @@ class EventFooterLinkForm(I18nModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = EventFooterLink
|
model = EventFooterLink
|
||||||
fields = ('label', 'url')
|
fields = ('label', 'url')
|
||||||
|
widgets = {
|
||||||
|
"url": forms.URLInput(
|
||||||
|
attrs={
|
||||||
|
"placeholder": "https://..."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class BaseEventFooterLinkFormSet(I18nFormSetMixin, forms.BaseInlineFormSet):
|
class BaseEventFooterLinkFormSet(I18nFormSetMixin, forms.BaseInlineFormSet):
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ from pretix.base.models import (
|
|||||||
SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher,
|
SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher,
|
||||||
)
|
)
|
||||||
from pretix.base.signals import register_payment_providers
|
from pretix.base.signals import register_payment_providers
|
||||||
|
from pretix.base.timeframes import (
|
||||||
|
DateFrameField,
|
||||||
|
resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
|
||||||
|
)
|
||||||
from pretix.control.forms import SplitDateTimeField
|
from pretix.control.forms import SplitDateTimeField
|
||||||
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
|
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
|
||||||
from pretix.control.signals import order_search_filter_q
|
from pretix.control.signals import order_search_filter_q
|
||||||
@@ -1219,6 +1223,129 @@ class OrderPaymentSearchFilterForm(forms.Form):
|
|||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionAnswerFilterForm(forms.Form):
|
||||||
|
STATUS_VARIANTS = [
|
||||||
|
("", _("All orders")),
|
||||||
|
(Order.STATUS_PAID, _("Paid")),
|
||||||
|
(Order.STATUS_PAID + 'v', _("Paid or confirmed")),
|
||||||
|
(Order.STATUS_PENDING, _("Pending")),
|
||||||
|
(Order.STATUS_PENDING + Order.STATUS_PAID, _("Pending or paid")),
|
||||||
|
("o", _("Pending (overdue)")),
|
||||||
|
(Order.STATUS_EXPIRED, _("Expired")),
|
||||||
|
(Order.STATUS_PENDING + Order.STATUS_EXPIRED, _("Pending or expired")),
|
||||||
|
(Order.STATUS_CANCELED, _("Canceled"))
|
||||||
|
]
|
||||||
|
|
||||||
|
status = forms.ChoiceField(
|
||||||
|
choices=STATUS_VARIANTS,
|
||||||
|
required=False,
|
||||||
|
label=_("Order status"),
|
||||||
|
)
|
||||||
|
item = forms.ChoiceField(
|
||||||
|
choices=[],
|
||||||
|
required=False,
|
||||||
|
label=_("Products"),
|
||||||
|
)
|
||||||
|
subevent = forms.ModelChoiceField(
|
||||||
|
queryset=SubEvent.objects.none(),
|
||||||
|
required=False,
|
||||||
|
empty_label=pgettext_lazy('subevent', 'All dates'),
|
||||||
|
label=pgettext_lazy("subevent", "Date"),
|
||||||
|
)
|
||||||
|
date_range = DateFrameField(
|
||||||
|
required=False,
|
||||||
|
include_future_frames=True,
|
||||||
|
label=_('Event date'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.event = kwargs.pop('event')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.initial['status'] = Order.STATUS_PENDING + Order.STATUS_PAID
|
||||||
|
|
||||||
|
choices = [('', _('All products'))]
|
||||||
|
for i in self.event.items.prefetch_related('variations').all():
|
||||||
|
variations = list(i.variations.all())
|
||||||
|
if variations:
|
||||||
|
choices.append((str(i.pk), _('{product} – Any variation').format(product=str(i))))
|
||||||
|
for v in variations:
|
||||||
|
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (str(i), v.value)))
|
||||||
|
else:
|
||||||
|
choices.append((str(i.pk), str(i)))
|
||||||
|
self.fields['item'].choices = choices
|
||||||
|
|
||||||
|
if self.event.has_subevents:
|
||||||
|
self.fields["subevent"].queryset = self.event.subevents.all()
|
||||||
|
self.fields['subevent'].widget = Select2(
|
||||||
|
attrs={
|
||||||
|
'data-model-select2': 'event',
|
||||||
|
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
|
||||||
|
'event': self.event.slug,
|
||||||
|
'organizer': self.event.organizer.slug,
|
||||||
|
}),
|
||||||
|
'data-placeholder': pgettext_lazy('subevent', 'All dates')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
|
||||||
|
else:
|
||||||
|
del self.fields['subevent']
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
subevent = cleaned_data.get('subevent')
|
||||||
|
date_range = cleaned_data.get('date_range')
|
||||||
|
|
||||||
|
if subevent is not None and date_range is not None:
|
||||||
|
d_start, d_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), date_range, self.event.timezone)
|
||||||
|
if (
|
||||||
|
(d_start and not (d_start <= subevent.date_from)) or
|
||||||
|
(d_end and not (subevent.date_from < d_end))
|
||||||
|
):
|
||||||
|
self.add_error('subevent', pgettext_lazy('subevent', "Date doesn't start in selected date range."))
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
def filter_qs(self, opqs):
|
||||||
|
fdata = self.cleaned_data
|
||||||
|
|
||||||
|
subevent = fdata.get('subevent', None)
|
||||||
|
date_range = fdata.get('date_range', None)
|
||||||
|
|
||||||
|
if subevent is not None:
|
||||||
|
opqs = opqs.filter(subevent=subevent)
|
||||||
|
|
||||||
|
if date_range is not None:
|
||||||
|
d_start, d_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), date_range, self.event.timezone)
|
||||||
|
opqs = opqs.filter(
|
||||||
|
subevent__date_from__gte=d_start,
|
||||||
|
subevent__date_from__lt=d_end
|
||||||
|
)
|
||||||
|
|
||||||
|
s = fdata.get("status", Order.STATUS_PENDING + Order.STATUS_PAID)
|
||||||
|
if s != "":
|
||||||
|
if s == Order.STATUS_PENDING:
|
||||||
|
opqs = opqs.filter(order__status=Order.STATUS_PENDING,
|
||||||
|
order__expires__lt=now().replace(hour=0, minute=0, second=0))
|
||||||
|
elif s == Order.STATUS_PENDING + Order.STATUS_PAID:
|
||||||
|
opqs = opqs.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID])
|
||||||
|
elif s == Order.STATUS_PAID + 'v':
|
||||||
|
opqs = opqs.filter(
|
||||||
|
Q(order__status=Order.STATUS_PAID) |
|
||||||
|
Q(order__status=Order.STATUS_PENDING, order__valid_if_pending=True)
|
||||||
|
)
|
||||||
|
elif s == Order.STATUS_PENDING + Order.STATUS_EXPIRED:
|
||||||
|
opqs = opqs.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_EXPIRED])
|
||||||
|
else:
|
||||||
|
opqs = opqs.filter(order__status=s)
|
||||||
|
|
||||||
|
if s not in (Order.STATUS_CANCELED, ""):
|
||||||
|
opqs = opqs.filter(canceled=False)
|
||||||
|
if fdata.get("item", "") != "":
|
||||||
|
i = fdata.get("item", "")
|
||||||
|
opqs = opqs.filter(item_id__in=(i,))
|
||||||
|
|
||||||
|
return opqs
|
||||||
|
|
||||||
|
|
||||||
class SubEventFilterForm(FilterForm):
|
class SubEventFilterForm(FilterForm):
|
||||||
orders = {
|
orders = {
|
||||||
'date_from': 'date_from',
|
'date_from': 'date_from',
|
||||||
|
|||||||
@@ -474,6 +474,7 @@ class OrganizerSettingsForm(SettingsForm):
|
|||||||
'customer_accounts',
|
'customer_accounts',
|
||||||
'customer_accounts_native',
|
'customer_accounts_native',
|
||||||
'customer_accounts_link_by_email',
|
'customer_accounts_link_by_email',
|
||||||
|
'customer_accounts_require_login_for_order_access',
|
||||||
'invoice_regenerate_allowed',
|
'invoice_regenerate_allowed',
|
||||||
'contact_mail',
|
'contact_mail',
|
||||||
'imprint_url',
|
'imprint_url',
|
||||||
@@ -1024,6 +1025,13 @@ class OrganizerFooterLinkForm(I18nModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = OrganizerFooterLink
|
model = OrganizerFooterLink
|
||||||
fields = ('label', 'url')
|
fields = ('label', 'url')
|
||||||
|
widgets = {
|
||||||
|
"url": forms.URLInput(
|
||||||
|
attrs={
|
||||||
|
"placeholder": "https://..."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class BaseOrganizerFooterLinkFormSet(I18nFormSetMixin, forms.BaseInlineFormSet):
|
class BaseOrganizerFooterLinkFormSet(I18nFormSetMixin, forms.BaseInlineFormSet):
|
||||||
|
|||||||
@@ -814,7 +814,7 @@ class OrganizerPluginStateLogEntryType(LogEntryType):
|
|||||||
if app and hasattr(app, 'PretixPluginMeta'):
|
if app and hasattr(app, 'PretixPluginMeta'):
|
||||||
return {
|
return {
|
||||||
'href': reverse('control:organizer.settings.plugins', kwargs={
|
'href': reverse('control:organizer.settings.plugins', kwargs={
|
||||||
'organizer': logentry.event.organizer.slug,
|
'organizer': logentry.organizer.slug,
|
||||||
}) + '#plugin_' + logentry.parsed_data['plugin'],
|
}) + '#plugin_' + logentry.parsed_data['plugin'],
|
||||||
'val': app.PretixPluginMeta.name
|
'val': app.PretixPluginMeta.name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
{% bootstrap_field formset.empty_form.overwrite layout='inline' form_group_class="" %}
|
{% bootstrap_field formset.empty_form.overwrite layout='inline' form_group_class="" %}
|
||||||
</div>
|
</div>
|
||||||
{{ f.value_map.as_hidden }}
|
{{ formset.empty_form.value_map.as_hidden }}
|
||||||
<div class="col-md-2 text-right flip">
|
<div class="col-md-2 text-right flip">
|
||||||
<i class="fa fa-warning hidden" data-toggle="tooltip" title=""></i>
|
<i class="fa fa-warning hidden" data-toggle="tooltip" title=""></i>
|
||||||
<button type="button" class="btn btn-default hidden" data-edit-value-map data-toggle="modal"
|
<button type="button" class="btn btn-default hidden" data-edit-value-map data-toggle="modal"
|
||||||
|
|||||||
@@ -20,35 +20,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<form class="panel-body filter-form" action="" method="get">
|
<form class="panel-body filter-form" action="" method="get">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-2 col-sm-6 col-xs-6">
|
<div class="col-md-2 col-xs-6">
|
||||||
<select name="status" class="form-control">
|
{% bootstrap_field form.status %}
|
||||||
<option value="" {% if request.GET.status == "" %}selected="selected"{% endif %}>{% trans "All orders" %}</option>
|
</div>
|
||||||
<option value="p" {% if request.GET.status == "p" %}selected="selected"{% endif %}>{% trans "Paid" %}</option>
|
<div class="col-md-3 col-xs-6">
|
||||||
<option value="pv" {% if request.GET.status == "pv" %}selected="selected"{% endif %}>{% trans "Paid or confirmed" %}</option>
|
{% bootstrap_field form.item %}
|
||||||
<option value="n" {% if request.GET.status == "n" %}selected="selected"{% endif %}>{% trans "Pending" %}</option>
|
</div>
|
||||||
<option value="np" {% if request.GET.status == "np" or "status" not in request.GET %}selected="selected"{% endif %}>{% trans "Pending or paid" %}</option>
|
{% if has_subevents %}
|
||||||
<option value="o" {% if request.GET.status == "o" %}selected="selected"{% endif %}>{% trans "Pending (overdue)" %}</option>
|
<div class="col-md-3 col-xs-6">
|
||||||
<option value="e" {% if request.GET.status == "e" %}selected="selected"{% endif %}>{% trans "Expired" %}</option>
|
{% bootstrap_field form.subevent %}
|
||||||
<option value="ne" {% if request.GET.status == "ne" %}selected="selected"{% endif %}>{% trans "Pending or expired" %}</option>
|
|
||||||
<option value="c" {% if request.GET.status == "c" %}selected="selected"{% endif %}>{% trans "Canceled" %}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-5 col-sm-6 col-xs-6">
|
<div class="col-md-4 col-xs-6">
|
||||||
<select name="item" class="form-control">
|
{% bootstrap_field form.date_range %}
|
||||||
<option value="">{% trans "All products" %}</option>
|
|
||||||
{% for item in items %}
|
|
||||||
<option value="{{ item.id }}"
|
|
||||||
{% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}>
|
|
||||||
{{ item.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
{% if request.event.has_subevents %}
|
{% endif %}
|
||||||
<div class="col-lg-5 col-sm-6 col-xs-6">
|
|
||||||
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<button class="btn btn-primary btn-lg" type="submit">
|
<button class="btn btn-primary btn-lg" type="submit">
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
<legend>{% trans "Customer accounts" %}</legend>
|
<legend>{% trans "Customer accounts" %}</legend>
|
||||||
{% bootstrap_field sform.customer_accounts layout="control" %}
|
{% bootstrap_field sform.customer_accounts layout="control" %}
|
||||||
{% bootstrap_field sform.customer_accounts_native layout="control" %}
|
{% bootstrap_field sform.customer_accounts_native layout="control" %}
|
||||||
|
{% bootstrap_field sform.customer_accounts_require_login_for_order_access layout="control" %}
|
||||||
{% bootstrap_field sform.customer_accounts_link_by_email layout="control" %}
|
{% bootstrap_field sform.customer_accounts_link_by_email layout="control" %}
|
||||||
{% bootstrap_field sform.name_scheme layout="control" %}
|
{% bootstrap_field sform.name_scheme layout="control" %}
|
||||||
{% bootstrap_field sform.name_scheme_titles layout="control" %}
|
{% bootstrap_field sform.name_scheme_titles layout="control" %}
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ from pretix.base.models import (
|
|||||||
)
|
)
|
||||||
from pretix.base.services.quotas import QuotaAvailability
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
from pretix.base.timeline import timeline_for_event
|
from pretix.base.timeline import timeline_for_event
|
||||||
from pretix.control.forms.event import CommentForm
|
|
||||||
from pretix.control.signals import (
|
from pretix.control.signals import (
|
||||||
event_dashboard_widgets, user_dashboard_widgets,
|
event_dashboard_widgets, user_dashboard_widgets,
|
||||||
)
|
)
|
||||||
@@ -341,6 +340,8 @@ def welcome_wizard_widget(sender, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
def event_index(request, organizer, event):
|
def event_index(request, organizer, event):
|
||||||
|
from pretix.control.forms.event import CommentForm
|
||||||
|
|
||||||
subevent = None
|
subevent = None
|
||||||
if request.GET.get("subevent", "") != "" and request.event.has_subevents:
|
if request.GET.get("subevent", "") != "" and request.event.has_subevents:
|
||||||
i = request.GET.get("subevent", "")
|
i = request.GET.get("subevent", "")
|
||||||
|
|||||||
@@ -98,7 +98,6 @@ from pretix.control.views.mailsetup import MailSettingsSetupView
|
|||||||
from pretix.control.views.user import RecentAuthenticationRequiredMixin
|
from pretix.control.views.user import RecentAuthenticationRequiredMixin
|
||||||
from pretix.helpers.database import rolledback_transaction
|
from pretix.helpers.database import rolledback_transaction
|
||||||
from pretix.multidomain.urlreverse import build_absolute_uri, get_event_domain
|
from pretix.multidomain.urlreverse import build_absolute_uri, get_event_domain
|
||||||
from pretix.plugins.stripe.payment import StripeSettingsHolder
|
|
||||||
from pretix.presale.views.widget import (
|
from pretix.presale.views.widget import (
|
||||||
version_default as widget_version_default,
|
version_default as widget_version_default,
|
||||||
)
|
)
|
||||||
@@ -830,8 +829,8 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
|||||||
return locales
|
return locales
|
||||||
|
|
||||||
# get all supported placeholders with dummy values
|
# get all supported placeholders with dummy values
|
||||||
def placeholders(self, item):
|
def placeholders(self, item, rich=True):
|
||||||
return get_sample_context(self.request.event, MailSettingsForm.base_context[item])
|
return get_sample_context(self.request.event, MailSettingsForm.base_context[item], rich=rich)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
preview_item = request.POST.get('item', '')
|
preview_item = request.POST.get('item', '')
|
||||||
@@ -852,6 +851,14 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
|||||||
msgs[self.supported_locale[idx]] = prefix_subject(self.request.event, format_map(
|
msgs[self.supported_locale[idx]] = prefix_subject(self.request.event, format_map(
|
||||||
bleach.clean(v), self.placeholders(preview_item), raise_on_missing=True
|
bleach.clean(v), self.placeholders(preview_item), raise_on_missing=True
|
||||||
), highlight=True)
|
), highlight=True)
|
||||||
|
elif preview_item in MailSettingsForm.plain_rendering:
|
||||||
|
msgs[self.supported_locale[idx]] = mark_safe(
|
||||||
|
format_map(
|
||||||
|
conditional_escape(v),
|
||||||
|
self.placeholders(preview_item, rich=False),
|
||||||
|
raise_on_missing=True
|
||||||
|
).replace("\n", "<br />")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
placeholders = self.placeholders(preview_item)
|
placeholders = self.placeholders(preview_item)
|
||||||
msgs[self.supported_locale[idx]] = format_map(
|
msgs[self.supported_locale[idx]] = format_map(
|
||||||
@@ -1666,6 +1673,8 @@ class QuickSetupView(FormView):
|
|||||||
'or take your event live to start selling!'))
|
'or take your event live to start selling!'))
|
||||||
|
|
||||||
if form.cleaned_data.get('payment_stripe__enabled', False):
|
if form.cleaned_data.get('payment_stripe__enabled', False):
|
||||||
|
from pretix.plugins.stripe.payment import StripeSettingsHolder
|
||||||
|
|
||||||
self.request.session['payment_stripe_oauth_enable'] = True
|
self.request.session['payment_stripe_oauth_enable'] = True
|
||||||
return redirect(StripeSettingsHolder(self.request.event).get_connect_url(self.request))
|
return redirect(StripeSettingsHolder(self.request.event).get_connect_url(self.request))
|
||||||
|
|
||||||
|
|||||||
@@ -188,6 +188,8 @@ class LicenseCheckView(StaffMemberRequiredMixin, FormView):
|
|||||||
return None, None
|
return None, None
|
||||||
try:
|
try:
|
||||||
for k, v in pkg.metadata.items():
|
for k, v in pkg.metadata.items():
|
||||||
|
if k == "License-Expression":
|
||||||
|
license = v
|
||||||
if k == "License":
|
if k == "License":
|
||||||
license = v
|
license = v
|
||||||
if k == "Home-page":
|
if k == "Home-page":
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ from pretix.api.serializers.item import (
|
|||||||
)
|
)
|
||||||
from pretix.base.forms import I18nFormSet
|
from pretix.base.forms import I18nFormSet
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation, Order,
|
CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation,
|
||||||
OrderPosition, Question, QuestionAnswer, QuestionOption, Quota,
|
OrderPosition, Question, QuestionAnswer, QuestionOption, Quota,
|
||||||
SeatCategoryMapping, Voucher,
|
SeatCategoryMapping, Voucher,
|
||||||
)
|
)
|
||||||
@@ -74,6 +74,7 @@ from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
|
|||||||
from pretix.base.services.quotas import QuotaAvailability
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
from pretix.base.services.tickets import invalidate_cache
|
from pretix.base.services.tickets import invalidate_cache
|
||||||
from pretix.base.signals import quota_availability
|
from pretix.base.signals import quota_availability
|
||||||
|
from pretix.control.forms.filter import QuestionAnswerFilterForm
|
||||||
from pretix.control.forms.item import (
|
from pretix.control.forms.item import (
|
||||||
CategoryForm, ItemAddOnForm, ItemAddOnsFormSet, ItemBundleForm,
|
CategoryForm, ItemAddOnForm, ItemAddOnsFormSet, ItemBundleForm,
|
||||||
ItemBundleFormSet, ItemCreateForm, ItemMetaValueForm, ItemProgramTimeForm,
|
ItemBundleFormSet, ItemCreateForm, ItemMetaValueForm, ItemProgramTimeForm,
|
||||||
@@ -660,46 +661,26 @@ class QuestionMixin:
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
class QuestionView(EventPermissionRequiredMixin, QuestionMixin, ChartContainingView, DetailView):
|
class QuestionView(EventPermissionRequiredMixin, ChartContainingView, DetailView):
|
||||||
model = Question
|
model = Question
|
||||||
template_name = 'pretixcontrol/items/question.html'
|
template_name = 'pretixcontrol/items/question.html'
|
||||||
permission = 'can_change_items'
|
permission = 'can_change_items'
|
||||||
template_name_field = 'question'
|
template_name_field = 'question'
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def filter_form(self):
|
||||||
|
return QuestionAnswerFilterForm(event=self.request.event, data=self.request.GET)
|
||||||
|
|
||||||
def get_answer_statistics(self):
|
def get_answer_statistics(self):
|
||||||
opqs = OrderPosition.objects.filter(
|
opqs = OrderPosition.objects.filter(
|
||||||
order__event=self.request.event,
|
order__event=self.request.event,
|
||||||
)
|
)
|
||||||
|
if self.filter_form.is_valid():
|
||||||
|
opqs = self.filter_form.filter_qs(opqs)
|
||||||
|
|
||||||
qs = QuestionAnswer.objects.filter(
|
qs = QuestionAnswer.objects.filter(
|
||||||
question=self.object, orderposition__isnull=False,
|
question=self.object, orderposition__isnull=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.request.GET.get("subevent", "") != "":
|
|
||||||
opqs = opqs.filter(subevent=self.request.GET["subevent"])
|
|
||||||
|
|
||||||
s = self.request.GET.get("status", "np")
|
|
||||||
if s != "":
|
|
||||||
if s == 'o':
|
|
||||||
opqs = opqs.filter(order__status=Order.STATUS_PENDING,
|
|
||||||
order__expires__lt=now().replace(hour=0, minute=0, second=0))
|
|
||||||
elif s == 'np':
|
|
||||||
opqs = opqs.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID])
|
|
||||||
elif s == 'pv':
|
|
||||||
opqs = opqs.filter(
|
|
||||||
Q(order__status=Order.STATUS_PAID) |
|
|
||||||
Q(order__status=Order.STATUS_PENDING, order__valid_if_pending=True)
|
|
||||||
)
|
|
||||||
elif s == 'ne':
|
|
||||||
opqs = opqs.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_EXPIRED])
|
|
||||||
else:
|
|
||||||
opqs = opqs.filter(order__status=s)
|
|
||||||
|
|
||||||
if s not in (Order.STATUS_CANCELED, ""):
|
|
||||||
opqs = opqs.filter(canceled=False)
|
|
||||||
if self.request.GET.get("item", "") != "":
|
|
||||||
i = self.request.GET.get("item", "")
|
|
||||||
opqs = opqs.filter(item_id__in=(i,))
|
|
||||||
|
|
||||||
qs = qs.filter(orderposition__in=opqs)
|
qs = qs.filter(orderposition__in=opqs)
|
||||||
op_cnt = opqs.filter(item__in=self.object.items.all()).count()
|
op_cnt = opqs.filter(item__in=self.object.items.all()).count()
|
||||||
|
|
||||||
@@ -746,9 +727,11 @@ class QuestionView(EventPermissionRequiredMixin, QuestionMixin, ChartContainingV
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data()
|
ctx = super().get_context_data()
|
||||||
ctx['items'] = self.object.items.all()
|
ctx['items'] = self.object.items.exists()
|
||||||
|
ctx['has_subevents'] = self.request.event.has_subevents
|
||||||
stats = self.get_answer_statistics()
|
stats = self.get_answer_statistics()
|
||||||
ctx['stats'], ctx['total'] = stats
|
ctx['stats'], ctx['total'] = stats
|
||||||
|
ctx['form'] = self.filter_form
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def get_object(self, queryset=None) -> Question:
|
def get_object(self, queryset=None) -> Question:
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ from datetime import timedelta
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
from django.http import Http404
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
@@ -85,6 +86,7 @@ class BaseImportView(TemplateView):
|
|||||||
filename='import.csv',
|
filename='import.csv',
|
||||||
type='text/csv',
|
type='text/csv',
|
||||||
)
|
)
|
||||||
|
cf.bind_to_session(request, "modelimport")
|
||||||
cf.file.save('import.csv', request.FILES['file'])
|
cf.file.save('import.csv', request.FILES['file'])
|
||||||
|
|
||||||
if self.request.POST.get("charset") in ENCODINGS:
|
if self.request.POST.get("charset") in ENCODINGS:
|
||||||
@@ -137,7 +139,10 @@ class BaseProcessView(AsyncAction, FormView):
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def file(self):
|
def file(self):
|
||||||
return get_object_or_404(CachedFile, pk=self.kwargs.get("file"), filename="import.csv")
|
cf = get_object_or_404(CachedFile, pk=self.kwargs.get("file"), filename="import.csv")
|
||||||
|
if not cf.allowed_for_session(self.request, "modelimport"):
|
||||||
|
raise Http404()
|
||||||
|
return cf
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def parsed(self):
|
def parsed(self):
|
||||||
@@ -156,7 +161,7 @@ class BaseProcessView(AsyncAction, FormView):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
reader = parse_csv(self.file.file, 1024 * 1024, "replace", charset=charset)
|
reader = parse_csv(self.file.file, 1024 * 1024, "replace", charset=charset)
|
||||||
if reader._had_duplicates:
|
if reader and reader._had_duplicates:
|
||||||
messages.warning(
|
messages.warning(
|
||||||
self.request,
|
self.request,
|
||||||
_(
|
_(
|
||||||
|
|||||||
@@ -3016,7 +3016,7 @@ class EventCancel(EventPermissionRequiredMixin, AsyncAction, FormView):
|
|||||||
return _('All orders have been canceled.')
|
return _('All orders have been canceled.')
|
||||||
else:
|
else:
|
||||||
return _('The orders have been canceled. An error occurred with {count} orders, please '
|
return _('The orders have been canceled. An error occurred with {count} orders, please '
|
||||||
'check all uncanceled orders.').format(count=value)
|
'check all uncanceled orders.').format(count=value["failed"])
|
||||||
|
|
||||||
def get_success_url(self, value):
|
def get_success_url(self, value):
|
||||||
if settings.HAS_CELERY:
|
if settings.HAS_CELERY:
|
||||||
@@ -3097,7 +3097,7 @@ class EventCancelConfirm(EventPermissionRequiredMixin, AsyncAction, FormView):
|
|||||||
return _('All orders have been canceled.')
|
return _('All orders have been canceled.')
|
||||||
else:
|
else:
|
||||||
return _('The orders have been canceled. An error occurred with {count} orders, please '
|
return _('The orders have been canceled. An error occurred with {count} orders, please '
|
||||||
'check all uncanceled orders.').format(count=value)
|
'check all uncanceled orders.').format(count=value["failed"])
|
||||||
|
|
||||||
def get_success_url(self, value):
|
def get_success_url(self, value):
|
||||||
return reverse('control:event.cancel', kwargs={
|
return reverse('control:event.cancel', kwargs={
|
||||||
|
|||||||
@@ -2583,7 +2583,7 @@ class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
# technically, we'd also need to sort by pk since this is a paginated list, but in this case we just can't
|
# technically, we'd also need to sort by pk since this is a paginated list, but in this case we just can't
|
||||||
# bear the performance cost
|
# bear the performance cost
|
||||||
qs = self.request.organizer.all_logentries().select_related(
|
qs = self.request.organizer.logentry_set.filter(event=None).select_related(
|
||||||
'user', 'content_type', 'api_token', 'oauth_application', 'device'
|
'user', 'content_type', 'api_token', 'oauth_application', 'device'
|
||||||
).order_by('-datetime')
|
).order_by('-datetime')
|
||||||
qs = qs.exclude(action_type__in=OVERVIEW_BANLIST)
|
qs = qs.exclude(action_type__in=OVERVIEW_BANLIST)
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
|||||||
cf = None
|
cf = None
|
||||||
if request.POST.get("background", "").strip():
|
if request.POST.get("background", "").strip():
|
||||||
try:
|
try:
|
||||||
cf = CachedFile.objects.get(id=request.POST.get("background"))
|
cf = CachedFile.objects.get(id=request.POST.get("background"), web_download=True)
|
||||||
except CachedFile.DoesNotExist:
|
except CachedFile.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ from collections import OrderedDict
|
|||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.http import Http404
|
||||||
|
from django.shortcuts import redirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import get_language, gettext_lazy as _
|
from django.utils.translation import get_language, gettext_lazy as _
|
||||||
@@ -94,6 +95,8 @@ class ShredDownloadView(RecentAuthenticationRequiredMixin, EventPermissionRequir
|
|||||||
cf = CachedFile.objects.get(pk=kwargs['file'])
|
cf = CachedFile.objects.get(pk=kwargs['file'])
|
||||||
except CachedFile.DoesNotExist:
|
except CachedFile.DoesNotExist:
|
||||||
raise ShredError(_("The download file could no longer be found on the server, please try to start again."))
|
raise ShredError(_("The download file could no longer be found on the server, please try to start again."))
|
||||||
|
if not cf.allowed_for_session(self.request):
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
with ZipFile(cf.file.file, 'r') as zipfile:
|
with ZipFile(cf.file.file, 'r') as zipfile:
|
||||||
indexdata = json.loads(zipfile.read('index.json').decode())
|
indexdata = json.loads(zipfile.read('index.json').decode())
|
||||||
@@ -111,7 +114,7 @@ class ShredDownloadView(RecentAuthenticationRequiredMixin, EventPermissionRequir
|
|||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
ctx['shredders'] = self.shredders
|
ctx['shredders'] = self.shredders
|
||||||
ctx['download_on_shred'] = any(shredder.require_download_confirmation for shredder in shredders)
|
ctx['download_on_shred'] = any(shredder.require_download_confirmation for shredder in shredders)
|
||||||
ctx['file'] = get_object_or_404(CachedFile, pk=kwargs.get("file"))
|
ctx['file'] = cf
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -569,7 +569,7 @@ def category_select2(request, **kwargs):
|
|||||||
page = 1
|
page = 1
|
||||||
|
|
||||||
qs = request.event.categories.filter(
|
qs = request.event.categories.filter(
|
||||||
name__icontains=i18ncomp(query)
|
Q(name__icontains=i18ncomp(query)) | Q(internal_name__icontains=query)
|
||||||
).order_by('name')
|
).order_by('name')
|
||||||
|
|
||||||
total = qs.count()
|
total = qs.count()
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ from django.utils import translation
|
|||||||
from django.utils.translation import gettext_noop
|
from django.utils.translation import gettext_noop
|
||||||
from django_countries import Countries, collator
|
from django_countries import Countries, collator
|
||||||
from django_countries.fields import CountryField
|
from django_countries.fields import CountryField
|
||||||
from phonenumbers.data import _COUNTRY_CODE_TO_REGION_CODE
|
from phonenumbers import (
|
||||||
|
COUNTRY_CODE_TO_REGION_CODE, REGION_CODE_FOR_NON_GEO_ENTITY,
|
||||||
|
)
|
||||||
|
|
||||||
from pretix.base.i18n import get_babel_locale, get_language_without_region
|
from pretix.base.i18n import get_babel_locale, get_language_without_region
|
||||||
|
|
||||||
@@ -40,6 +42,10 @@ class CachedCountries(Countries):
|
|||||||
django-countries performs a unicode-aware sorting based on pyuca which is incredibly
|
django-countries performs a unicode-aware sorting based on pyuca which is incredibly
|
||||||
slow.
|
slow.
|
||||||
"""
|
"""
|
||||||
|
# Starting in django-countries 8.1, django-countries implemented a similar caching, but only on object level.
|
||||||
|
# We keep our caching for now for the added caching to the cache store. Unfortunately we can't really avoid
|
||||||
|
# the double-caching on object level if we want the caches od be used in a sensible order. We could re-evaluate
|
||||||
|
# and drop this in the future if it ever causes bugs or memory issues.
|
||||||
cache_key = "countries:all:{}".format(get_language_without_region())
|
cache_key = "countries:all:{}".format(get_language_without_region())
|
||||||
if self.cache_subkey:
|
if self.cache_subkey:
|
||||||
cache_key += ":" + self.cache_subkey
|
cache_key += ":" + self.cache_subkey
|
||||||
@@ -84,8 +90,6 @@ class FastCountryField(CountryField):
|
|||||||
*self._check_backend_specific_checks(**kwargs),
|
*self._check_backend_specific_checks(**kwargs),
|
||||||
*self._check_validators(),
|
*self._check_validators(),
|
||||||
*self._check_deprecation_details(),
|
*self._check_deprecation_details(),
|
||||||
*self._check_multiple(),
|
|
||||||
*self._check_max_length_attribute(**kwargs),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -107,9 +111,11 @@ def get_phone_prefixes_sorted_and_localized():
|
|||||||
val = []
|
val = []
|
||||||
|
|
||||||
locale = Locale(translation.to_locale(language))
|
locale = Locale(translation.to_locale(language))
|
||||||
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
|
for prefix, values in COUNTRY_CODE_TO_REGION_CODE.items():
|
||||||
prefix = "+%d" % prefix
|
prefix = "+%d" % prefix
|
||||||
for country_code in values:
|
for country_code in values:
|
||||||
|
if country_code == REGION_CODE_FOR_NON_GEO_ENTITY:
|
||||||
|
continue
|
||||||
country_name = locale.territories.get(country_code)
|
country_name = locale.territories.get(country_code)
|
||||||
if country_name:
|
if country_name:
|
||||||
val.append((prefix, "{} {}".format(country_name, prefix)))
|
val.append((prefix, "{} {}".format(country_name, prefix)))
|
||||||
|
|||||||
@@ -229,7 +229,8 @@ def get_language_score(locale):
|
|||||||
if not catalog:
|
if not catalog:
|
||||||
score = 1
|
score = 1
|
||||||
else:
|
else:
|
||||||
score = len(list(catalog.items())) or 1
|
source_strings = [k[1] if isinstance(k, tuple) else k for k in catalog.keys()]
|
||||||
|
score = len(set(source_strings)) or 1
|
||||||
return score
|
return score
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -100,3 +100,11 @@ class FontFallbackParagraph(Paragraph):
|
|||||||
if (original_font.endswith("Bd") or original_font.endswith(" B")) and "bold" in styles:
|
if (original_font.endswith("Bd") or original_font.endswith(" B")) and "bold" in styles:
|
||||||
return family + " B"
|
return family + " B"
|
||||||
return family
|
return family
|
||||||
|
|
||||||
|
|
||||||
|
def register_ttf_font_if_new(name, path):
|
||||||
|
from reportlab.pdfbase import pdfmetrics
|
||||||
|
from reportlab.pdfbase.ttfonts import TTFont
|
||||||
|
|
||||||
|
if name not in pdfmetrics.getRegisteredFontNames():
|
||||||
|
pdfmetrics.registerFont(TTFont(name, path))
|
||||||
|
|||||||
+1992
-1871
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
|
"POT-Creation-Date: 2026-01-05 12:13+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -140,7 +140,7 @@ msgstr ""
|
|||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:343
|
||||||
msgid "Confirming your payment …"
|
msgid "Confirming your payment …"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -344,7 +344,7 @@ msgstr ""
|
|||||||
msgid "close"
|
msgid "close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixbase/js/addressform.js:98
|
#: pretix/static/pretixbase/js/addressform.js:101
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
+2044
-1884
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
|
"POT-Creation-Date: 2026-01-05 12:13+0000\n"
|
||||||
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
|
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
|
||||||
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
|
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
|
||||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
@@ -144,7 +144,7 @@ msgstr "المتابعة"
|
|||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:343
|
||||||
msgid "Confirming your payment …"
|
msgid "Confirming your payment …"
|
||||||
msgstr "جاري تأكيد الدفع الخاص بك …"
|
msgstr "جاري تأكيد الدفع الخاص بك …"
|
||||||
|
|
||||||
@@ -359,7 +359,7 @@ msgstr "لا"
|
|||||||
msgid "close"
|
msgid "close"
|
||||||
msgstr "إغلاق"
|
msgstr "إغلاق"
|
||||||
|
|
||||||
#: pretix/static/pretixbase/js/addressform.js:98
|
#: pretix/static/pretixbase/js/addressform.js:101
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "مطلوب"
|
msgstr "مطلوب"
|
||||||
|
|||||||
+1992
-1871
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
|
"POT-Creation-Date: 2026-01-05 12:13+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -140,7 +140,7 @@ msgstr ""
|
|||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:343
|
||||||
msgid "Confirming your payment …"
|
msgid "Confirming your payment …"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -344,7 +344,7 @@ msgstr ""
|
|||||||
msgid "close"
|
msgid "close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixbase/js/addressform.js:98
|
#: pretix/static/pretixbase/js/addressform.js:101
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
+2038
-1886
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
|
"POT-Creation-Date: 2026-01-05 12:13+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-31 17:00+0000\n"
|
"PO-Revision-Date: 2025-10-31 17:00+0000\n"
|
||||||
"Last-Translator: Núria Masclans <nuriamasclansserrat@gmail.com>\n"
|
"Last-Translator: Núria Masclans <nuriamasclansserrat@gmail.com>\n"
|
||||||
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||||
@@ -141,7 +141,7 @@ msgstr "Continua"
|
|||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:343
|
||||||
msgid "Confirming your payment …"
|
msgid "Confirming your payment …"
|
||||||
msgstr "Confirmant el teu pagament…"
|
msgstr "Confirmant el teu pagament…"
|
||||||
|
|
||||||
@@ -345,7 +345,7 @@ msgstr "No"
|
|||||||
msgid "close"
|
msgid "close"
|
||||||
msgstr "Tanca"
|
msgstr "Tanca"
|
||||||
|
|
||||||
#: pretix/static/pretixbase/js/addressform.js:98
|
#: pretix/static/pretixbase/js/addressform.js:101
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "requerit"
|
msgstr "requerit"
|
||||||
|
|||||||
+2179
-2060
File diff suppressed because it is too large
Load Diff
@@ -7,9 +7,9 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
|
"POT-Creation-Date: 2026-01-05 12:13+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-08 18:57+0000\n"
|
"PO-Revision-Date: 2026-01-08 04:00+0000\n"
|
||||||
"Last-Translator: Alois Pospíšil <alois.pospisil@gmail.com>\n"
|
"Last-Translator: Jiří Pastrňák <jiri@pastrnak.email>\n"
|
||||||
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
"cs/>\n"
|
"cs/>\n"
|
||||||
"Language: cs\n"
|
"Language: cs\n"
|
||||||
@@ -17,7 +17,7 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
|
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
|
||||||
"X-Generator: Weblate 5.13\n"
|
"X-Generator: Weblate 5.15.1\n"
|
||||||
|
|
||||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||||
@@ -141,7 +141,7 @@ msgstr "Pokračovat"
|
|||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:343
|
||||||
msgid "Confirming your payment …"
|
msgid "Confirming your payment …"
|
||||||
msgstr "Potvrzuji vaši platbu …"
|
msgstr "Potvrzuji vaši platbu …"
|
||||||
|
|
||||||
@@ -345,7 +345,7 @@ msgstr "Ne"
|
|||||||
msgid "close"
|
msgid "close"
|
||||||
msgstr "zavřít"
|
msgstr "zavřít"
|
||||||
|
|
||||||
#: pretix/static/pretixbase/js/addressform.js:98
|
#: pretix/static/pretixbase/js/addressform.js:101
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "povinný"
|
msgstr "povinný"
|
||||||
@@ -416,7 +416,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: pretix/static/pretixbase/js/asynctask.js:276
|
#: pretix/static/pretixbase/js/asynctask.js:276
|
||||||
msgid "If this takes longer than a few minutes, please contact us."
|
msgid "If this takes longer than a few minutes, please contact us."
|
||||||
msgstr ""
|
msgstr "Pokud akce trvá déle než několik minut, kontaktujte nás."
|
||||||
|
|
||||||
#: pretix/static/pretixbase/js/asynctask.js:331
|
#: pretix/static/pretixbase/js/asynctask.js:331
|
||||||
msgid "Close message"
|
msgid "Close message"
|
||||||
@@ -734,7 +734,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/cart.js:49
|
#: pretix/static/pretixpresale/js/ui/cart.js:49
|
||||||
msgid "Cart expired"
|
msgid "Cart expired"
|
||||||
msgstr "Nákupní košík vypršel"
|
msgstr "Rezervace košíku vypršela"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/cart.js:58
|
#: pretix/static/pretixpresale/js/ui/cart.js:58
|
||||||
#: pretix/static/pretixpresale/js/ui/cart.js:84
|
#: pretix/static/pretixpresale/js/ui/cart.js:84
|
||||||
@@ -753,27 +753,23 @@ msgstr[2] ""
|
|||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/cart.js:83
|
#: pretix/static/pretixpresale/js/ui/cart.js:83
|
||||||
msgid "Your cart has expired."
|
msgid "Your cart has expired."
|
||||||
msgstr "Nákupní košík vypršel."
|
msgstr "Platnost rezervace vašeho košíku vypršela."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/cart.js:86
|
#: pretix/static/pretixpresale/js/ui/cart.js:86
|
||||||
#, fuzzy
|
|
||||||
#| msgid ""
|
|
||||||
#| "The items in your cart are no longer reserved for you. You can still "
|
|
||||||
#| "complete your order as long as they’re available."
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"The items in your cart are no longer reserved for you. You can still "
|
"The items in your cart are no longer reserved for you. You can still "
|
||||||
"complete your order as long as they're available."
|
"complete your order as long as they're available."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Produkty v nákupním košíku již nejsou pro vás rezervovány. Pokud je lístek "
|
"Produkty v nákupním košíku již nejsou rezervovány. Svou objednávku se přesto "
|
||||||
"stále dostupný, můžete objednávku dokončit."
|
"můžete pokusit dokončit, některé položky však už nemusí být dostupné."
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/cart.js:87
|
#: pretix/static/pretixpresale/js/ui/cart.js:87
|
||||||
msgid "Do you want to renew the reservation period?"
|
msgid "Do you want to renew the reservation period?"
|
||||||
msgstr ""
|
msgstr "Chcete obnovit rezervaci košíku?"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/cart.js:90
|
#: pretix/static/pretixpresale/js/ui/cart.js:90
|
||||||
msgid "Renew reservation"
|
msgid "Renew reservation"
|
||||||
msgstr ""
|
msgstr "Obnovit rezervaci"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:194
|
#: pretix/static/pretixpresale/js/ui/main.js:194
|
||||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||||
|
|||||||
+1996
-1871
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
|
"POT-Creation-Date: 2026-01-05 12:13+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -141,7 +141,7 @@ msgstr ""
|
|||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:343
|
||||||
msgid "Confirming your payment …"
|
msgid "Confirming your payment …"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -345,7 +345,7 @@ msgstr ""
|
|||||||
msgid "close"
|
msgid "close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixbase/js/addressform.js:98
|
#: pretix/static/pretixbase/js/addressform.js:101
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
+2045
-1875
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
|
"POT-Creation-Date: 2026-01-05 12:13+0000\n"
|
||||||
"PO-Revision-Date: 2024-07-10 15:00+0000\n"
|
"PO-Revision-Date: 2024-07-10 15:00+0000\n"
|
||||||
"Last-Translator: Nikolai <nikolai@lengefeldt.de>\n"
|
"Last-Translator: Nikolai <nikolai@lengefeldt.de>\n"
|
||||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
@@ -145,7 +145,7 @@ msgstr "Fortsæt"
|
|||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:343
|
||||||
msgid "Confirming your payment …"
|
msgid "Confirming your payment …"
|
||||||
msgstr "Bekræfter din betaling …"
|
msgstr "Bekræfter din betaling …"
|
||||||
|
|
||||||
@@ -357,7 +357,7 @@ msgstr "Nej"
|
|||||||
msgid "close"
|
msgid "close"
|
||||||
msgstr "Luk"
|
msgstr "Luk"
|
||||||
|
|
||||||
#: pretix/static/pretixbase/js/addressform.js:98
|
#: pretix/static/pretixbase/js/addressform.js:101
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Cart expired"
|
#| msgid "Cart expired"
|
||||||
|
|||||||
+2049
-1896
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
|
"POT-Creation-Date: 2026-01-05 12:13+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-29 09:24+0000\n"
|
"PO-Revision-Date: 2025-10-29 09:24+0000\n"
|
||||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
@@ -141,7 +141,7 @@ msgstr "Fortfahren"
|
|||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:343
|
||||||
msgid "Confirming your payment …"
|
msgid "Confirming your payment …"
|
||||||
msgstr "Zahlung wird bestätigt …"
|
msgstr "Zahlung wird bestätigt …"
|
||||||
|
|
||||||
@@ -345,7 +345,7 @@ msgstr "Nein"
|
|||||||
msgid "close"
|
msgid "close"
|
||||||
msgstr "schließen"
|
msgstr "schließen"
|
||||||
|
|
||||||
#: pretix/static/pretixbase/js/addressform.js:98
|
#: pretix/static/pretixbase/js/addressform.js:101
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "erforderlich"
|
msgstr "erforderlich"
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ Footer
|
|||||||
Footer-Link
|
Footer-Link
|
||||||
Footer-Text
|
Footer-Text
|
||||||
Galicisch
|
Galicisch
|
||||||
|
GBP
|
||||||
Geocoding
|
Geocoding
|
||||||
Geocoding-Daten
|
Geocoding-Daten
|
||||||
Geo-Koordinaten
|
Geo-Koordinaten
|
||||||
@@ -255,6 +256,7 @@ NFC-Chip
|
|||||||
NFC-Chips
|
NFC-Chips
|
||||||
NFC-Medien
|
NFC-Medien
|
||||||
NFC-Zahlungsmitteln
|
NFC-Zahlungsmitteln
|
||||||
|
NIF
|
||||||
Nr
|
Nr
|
||||||
NREI
|
NREI
|
||||||
number
|
number
|
||||||
@@ -263,6 +265,7 @@ Objekt-IDs
|
|||||||
Offline-Scan
|
Offline-Scan
|
||||||
OK
|
OK
|
||||||
Online-Banking
|
Online-Banking
|
||||||
|
Onlinebanking
|
||||||
Onlinebanking-Zugangsdaten
|
Onlinebanking-Zugangsdaten
|
||||||
Open
|
Open
|
||||||
OpenCage-API-Key
|
OpenCage-API-Key
|
||||||
@@ -292,6 +295,7 @@ Peppol-Netzwerk
|
|||||||
Peppol-Teilnehmer-ID
|
Peppol-Teilnehmer-ID
|
||||||
Peppol-Teilnehmer-IDs
|
Peppol-Teilnehmer-IDs
|
||||||
Personalisierung
|
Personalisierung
|
||||||
|
P.IVA
|
||||||
PKCE-Erweiterung
|
PKCE-Erweiterung
|
||||||
Platzhalterzeichen
|
Platzhalterzeichen
|
||||||
Play
|
Play
|
||||||
@@ -330,6 +334,9 @@ pretix-Versionen
|
|||||||
pretix-Widget
|
pretix-Widget
|
||||||
Produkt-ID
|
Produkt-ID
|
||||||
Produkt-Metadaten
|
Produkt-Metadaten
|
||||||
|
PromptPay
|
||||||
|
PromptPay-QR-Code
|
||||||
|
PromptPay-Zahlung
|
||||||
Przelewy
|
Przelewy
|
||||||
pt
|
pt
|
||||||
px
|
px
|
||||||
@@ -449,6 +456,7 @@ Ticketing
|
|||||||
Ticketing-Firma
|
Ticketing-Firma
|
||||||
Ticket-Output
|
Ticket-Output
|
||||||
Ticket-QR-Code
|
Ticket-QR-Code
|
||||||
|
TIN
|
||||||
TODO
|
TODO
|
||||||
To-Do-Liste
|
To-Do-Liste
|
||||||
TOTP
|
TOTP
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
|
"POT-Creation-Date: 2026-01-05 12:13+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-29 09:24+0000\n"
|
"PO-Revision-Date: 2025-10-29 09:24+0000\n"
|
||||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||||
@@ -141,7 +141,7 @@ msgstr "Fortfahren"
|
|||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:343
|
||||||
msgid "Confirming your payment …"
|
msgid "Confirming your payment …"
|
||||||
msgstr "Zahlung wird bestätigt …"
|
msgstr "Zahlung wird bestätigt …"
|
||||||
|
|
||||||
@@ -345,7 +345,7 @@ msgstr "Nein"
|
|||||||
msgid "close"
|
msgid "close"
|
||||||
msgstr "schließen"
|
msgstr "schließen"
|
||||||
|
|
||||||
#: pretix/static/pretixbase/js/addressform.js:98
|
#: pretix/static/pretixbase/js/addressform.js:101
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "erforderlich"
|
msgstr "erforderlich"
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ Footer
|
|||||||
Footer-Link
|
Footer-Link
|
||||||
Footer-Text
|
Footer-Text
|
||||||
Galicisch
|
Galicisch
|
||||||
|
GBP
|
||||||
Geocoding
|
Geocoding
|
||||||
Geocoding-Daten
|
Geocoding-Daten
|
||||||
Geo-Koordinaten
|
Geo-Koordinaten
|
||||||
@@ -255,6 +256,7 @@ NFC-Chip
|
|||||||
NFC-Chips
|
NFC-Chips
|
||||||
NFC-Medien
|
NFC-Medien
|
||||||
NFC-Zahlungsmitteln
|
NFC-Zahlungsmitteln
|
||||||
|
NIF
|
||||||
Nr
|
Nr
|
||||||
NREI
|
NREI
|
||||||
number
|
number
|
||||||
@@ -263,6 +265,7 @@ Objekt-IDs
|
|||||||
Offline-Scan
|
Offline-Scan
|
||||||
OK
|
OK
|
||||||
Online-Banking
|
Online-Banking
|
||||||
|
Onlinebanking
|
||||||
Onlinebanking-Zugangsdaten
|
Onlinebanking-Zugangsdaten
|
||||||
Open
|
Open
|
||||||
OpenCage-API-Key
|
OpenCage-API-Key
|
||||||
@@ -292,6 +295,7 @@ Peppol-Netzwerk
|
|||||||
Peppol-Teilnehmer-ID
|
Peppol-Teilnehmer-ID
|
||||||
Peppol-Teilnehmer-IDs
|
Peppol-Teilnehmer-IDs
|
||||||
Personalisierung
|
Personalisierung
|
||||||
|
P.IVA
|
||||||
PKCE-Erweiterung
|
PKCE-Erweiterung
|
||||||
Platzhalterzeichen
|
Platzhalterzeichen
|
||||||
Play
|
Play
|
||||||
@@ -330,6 +334,9 @@ pretix-Versionen
|
|||||||
pretix-Widget
|
pretix-Widget
|
||||||
Produkt-ID
|
Produkt-ID
|
||||||
Produkt-Metadaten
|
Produkt-Metadaten
|
||||||
|
PromptPay
|
||||||
|
PromptPay-QR-Code
|
||||||
|
PromptPay-Zahlung
|
||||||
Przelewy
|
Przelewy
|
||||||
pt
|
pt
|
||||||
px
|
px
|
||||||
@@ -449,6 +456,7 @@ Ticketing
|
|||||||
Ticketing-Firma
|
Ticketing-Firma
|
||||||
Ticket-Output
|
Ticket-Output
|
||||||
Ticket-QR-Code
|
Ticket-QR-Code
|
||||||
|
TIN
|
||||||
TODO
|
TODO
|
||||||
To-Do-Liste
|
To-Do-Liste
|
||||||
TOTP
|
TOTP
|
||||||
|
|||||||
+1992
-1871
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-27 13:58+0000\n"
|
"POT-Creation-Date: 2026-01-05 12:13+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -140,7 +140,7 @@ msgstr ""
|
|||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:343
|
||||||
msgid "Confirming your payment …"
|
msgid "Confirming your payment …"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -344,7 +344,7 @@ msgstr ""
|
|||||||
msgid "close"
|
msgid "close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixbase/js/addressform.js:98
|
#: pretix/static/pretixbase/js/addressform.js:101
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
+2039
-1884
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
|
"POT-Creation-Date: 2026-01-05 12:13+0000\n"
|
||||||
"PO-Revision-Date: 2024-12-22 00:00+0000\n"
|
"PO-Revision-Date: 2024-12-22 00:00+0000\n"
|
||||||
"Last-Translator: Dimitris Tsimpidis <tsimpidisd@gmail.com>\n"
|
"Last-Translator: Dimitris Tsimpidis <tsimpidisd@gmail.com>\n"
|
||||||
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
@@ -144,7 +144,7 @@ msgstr "Συνέχεια"
|
|||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:343
|
||||||
msgid "Confirming your payment …"
|
msgid "Confirming your payment …"
|
||||||
msgstr "Επιβεβαίωση πληρωμής…"
|
msgstr "Επιβεβαίωση πληρωμής…"
|
||||||
|
|
||||||
@@ -361,7 +361,7 @@ msgstr "Όχι"
|
|||||||
msgid "close"
|
msgid "close"
|
||||||
msgstr "Κλείσιμο"
|
msgstr "Κλείσιμο"
|
||||||
|
|
||||||
#: pretix/static/pretixbase/js/addressform.js:98
|
#: pretix/static/pretixbase/js/addressform.js:101
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Cart expired"
|
#| msgid "Cart expired"
|
||||||
|
|||||||
+1992
-1871
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
|
"POT-Creation-Date: 2026-01-05 12:13+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -140,7 +140,7 @@ msgstr ""
|
|||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:343
|
||||||
msgid "Confirming your payment …"
|
msgid "Confirming your payment …"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -344,7 +344,7 @@ msgstr ""
|
|||||||
msgid "close"
|
msgid "close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixbase/js/addressform.js:98
|
#: pretix/static/pretixbase/js/addressform.js:101
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
+2044
-1885
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
|
"POT-Creation-Date: 2026-01-05 12:13+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-22 16:00+0000\n"
|
"PO-Revision-Date: 2026-01-14 00:00+0000\n"
|
||||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
|
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||||
"js/es/>\n"
|
"js/es/>\n"
|
||||||
@@ -17,7 +17,7 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
"X-Generator: Weblate 5.13.3\n"
|
"X-Generator: Weblate 5.15.1\n"
|
||||||
|
|
||||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||||
@@ -141,7 +141,7 @@ msgstr "Continuar"
|
|||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:343
|
||||||
msgid "Confirming your payment …"
|
msgid "Confirming your payment …"
|
||||||
msgstr "Confirmando el pago…"
|
msgstr "Confirmando el pago…"
|
||||||
|
|
||||||
@@ -345,7 +345,7 @@ msgstr "No"
|
|||||||
msgid "close"
|
msgid "close"
|
||||||
msgstr "cerrar"
|
msgstr "cerrar"
|
||||||
|
|
||||||
#: pretix/static/pretixbase/js/addressform.js:98
|
#: pretix/static/pretixbase/js/addressform.js:101
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "campo requerido"
|
msgstr "campo requerido"
|
||||||
@@ -738,11 +738,8 @@ msgstr "Su carrito está a punto de caducar."
|
|||||||
#: pretix/static/pretixpresale/js/ui/cart.js:62
|
#: pretix/static/pretixpresale/js/ui/cart.js:62
|
||||||
msgid "The items in your cart are reserved for you for one minute."
|
msgid "The items in your cart are reserved for you for one minute."
|
||||||
msgid_plural "The items in your cart are reserved for you for {num} minutes."
|
msgid_plural "The items in your cart are reserved for you for {num} minutes."
|
||||||
msgstr[0] ""
|
msgstr[0] "Los artículos de la cesta están reservados durante un minuto."
|
||||||
"Los elementos en su carrito de compras se han reservado durante un minuto."
|
msgstr[1] "Los artículos de la cesta están reservados durante {num} minutos."
|
||||||
msgstr[1] ""
|
|
||||||
"Los elementos en su carrito de compras se han reservado durante {num} "
|
|
||||||
"minutos."
|
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/cart.js:83
|
#: pretix/static/pretixpresale/js/ui/cart.js:83
|
||||||
msgid "Your cart has expired."
|
msgid "Your cart has expired."
|
||||||
@@ -766,11 +763,11 @@ msgstr "Renovar reserva"
|
|||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:194
|
#: pretix/static/pretixpresale/js/ui/main.js:194
|
||||||
msgid "The organizer keeps %(currency)s %(amount)s"
|
msgid "The organizer keeps %(currency)s %(amount)s"
|
||||||
msgstr "El organizador se queda %(currency)s %(price)s"
|
msgstr "El organizador retiene %(currency)s %(amount)s"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:202
|
#: pretix/static/pretixpresale/js/ui/main.js:202
|
||||||
msgid "You get %(currency)s %(amount)s back"
|
msgid "You get %(currency)s %(amount)s back"
|
||||||
msgstr "Obtienes %(currency)s %(price)s de vuelta"
|
msgstr "Se le devolverá %(moneda)s %(cantidad)s"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:218
|
#: pretix/static/pretixpresale/js/ui/main.js:218
|
||||||
msgid "Please enter the amount the organizer can keep."
|
msgid "Please enter the amount the organizer can keep."
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
|
"POT-Creation-Date: 2026-01-05 12:13+0000\n"
|
||||||
"PO-Revision-Date: 2025-08-04 14:16+0200\n"
|
"PO-Revision-Date: 2025-08-04 14:16+0200\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
@@ -141,7 +141,7 @@ msgstr ""
|
|||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:343
|
||||||
msgid "Confirming your payment …"
|
msgid "Confirming your payment …"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -345,7 +345,7 @@ msgstr ""
|
|||||||
msgid "close"
|
msgid "close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixbase/js/addressform.js:98
|
#: pretix/static/pretixbase/js/addressform.js:101
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
+1999
-1878
File diff suppressed because it is too large
Load Diff
@@ -3,34 +3,35 @@
|
|||||||
# This file is distributed under the same license as the PACKAGE package.
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
#
|
#
|
||||||
#, fuzzy
|
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
|
"POT-Creation-Date: 2026-01-05 12:13+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: 2025-12-24 00:00+0000\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: Estonian <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||||
"Language: \n"
|
"js/et/>\n"
|
||||||
|
"Language: et\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
|
"X-Generator: Weblate 5.15.1\n"
|
||||||
|
|
||||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:68
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:68
|
||||||
msgid "Marked as paid"
|
msgid "Marked as paid"
|
||||||
msgstr ""
|
msgstr "Märgitud tasutuks"
|
||||||
|
|
||||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:76
|
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:76
|
||||||
msgid "Comment:"
|
msgid "Comment:"
|
||||||
msgstr ""
|
msgstr "Kommentaar:"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:34
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:34
|
||||||
msgid "PayPal"
|
msgid "PayPal"
|
||||||
msgstr ""
|
msgstr "PayPal"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:35
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:35
|
||||||
msgid "Venmo"
|
msgid "Venmo"
|
||||||
@@ -140,7 +141,7 @@ msgstr ""
|
|||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:343
|
||||||
msgid "Confirming your payment …"
|
msgid "Confirming your payment …"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -344,7 +345,7 @@ msgstr ""
|
|||||||
msgid "close"
|
msgid "close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixbase/js/addressform.js:98
|
#: pretix/static/pretixbase/js/addressform.js:101
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
+2054
-1879
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
|
"POT-Creation-Date: 2026-01-05 12:13+0000\n"
|
||||||
"PO-Revision-Date: 2024-09-06 08:47+0000\n"
|
"PO-Revision-Date: 2024-09-06 08:47+0000\n"
|
||||||
"Last-Translator: Albizuri <oier@puntu.eus>\n"
|
"Last-Translator: Albizuri <oier@puntu.eus>\n"
|
||||||
"Language-Team: Basque <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Basque <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
@@ -141,7 +141,7 @@ msgstr "Jarraitu"
|
|||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:343
|
||||||
msgid "Confirming your payment …"
|
msgid "Confirming your payment …"
|
||||||
msgstr "Ordainketa egiaztatzen …"
|
msgstr "Ordainketa egiaztatzen …"
|
||||||
|
|
||||||
@@ -345,7 +345,7 @@ msgstr "Ez"
|
|||||||
msgid "close"
|
msgid "close"
|
||||||
msgstr "itxi"
|
msgstr "itxi"
|
||||||
|
|
||||||
#: pretix/static/pretixbase/js/addressform.js:98
|
#: pretix/static/pretixbase/js/addressform.js:101
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
+2060
-1880
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-11-20 10:37+0000\n"
|
"POT-Creation-Date: 2026-01-05 12:13+0000\n"
|
||||||
"PO-Revision-Date: 2021-11-10 05:00+0000\n"
|
"PO-Revision-Date: 2021-11-10 05:00+0000\n"
|
||||||
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
|
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
|
||||||
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix-"
|
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||||
@@ -146,7 +146,7 @@ msgstr "Jatka"
|
|||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:267
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:284
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:317
|
||||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:341
|
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:343
|
||||||
msgid "Confirming your payment …"
|
msgid "Confirming your payment …"
|
||||||
msgstr "Maksuasi vahvistetaan …"
|
msgstr "Maksuasi vahvistetaan …"
|
||||||
|
|
||||||
@@ -363,7 +363,7 @@ msgstr "Ei"
|
|||||||
msgid "close"
|
msgid "close"
|
||||||
msgstr "Sulje"
|
msgstr "Sulje"
|
||||||
|
|
||||||
#: pretix/static/pretixbase/js/addressform.js:98
|
#: pretix/static/pretixbase/js/addressform.js:101
|
||||||
#: pretix/static/pretixpresale/js/ui/main.js:529
|
#: pretix/static/pretixpresale/js/ui/main.js:529
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Cart expired"
|
#| msgid "Cart expired"
|
||||||
|
|||||||
+1992
-1871
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user