mirror of
https://github.com/pretix/pretix.git
synced 2026-08-09 10:37:50 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b27a059ccb | ||
|
|
b8dbda4eb4 | ||
|
|
82d4a09da9 | ||
|
|
9dc554dfa8 | ||
|
|
7b05af6bfc |
@@ -24,7 +24,7 @@ jobs:
|
|||||||
name: Packaging
|
name: Packaging
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.13"]
|
python-version: ["3.11"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
|||||||
@@ -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.13
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.13
|
python-version: 3.11
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ jobs:
|
|||||||
name: Check gettext syntax
|
name: Check gettext syntax
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.13
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.13
|
python-version: 3.11
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
@@ -49,10 +49,10 @@ jobs:
|
|||||||
name: Spellcheck
|
name: Spellcheck
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.13
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.13
|
python-version: 3.11
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
|
|||||||
@@ -24,10 +24,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.13
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.13
|
python-version: 3.11
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
@@ -44,10 +44,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.13
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.13
|
python-version: 3.11
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
@@ -64,10 +64,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.13
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.13
|
python-version: 3.11
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pip3 install licenseheaders
|
run: pip3 install licenseheaders
|
||||||
- name: Run licenseheaders
|
- name: Run licenseheaders
|
||||||
|
|||||||
@@ -23,15 +23,13 @@ jobs:
|
|||||||
name: Tests
|
name: Tests
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.11", "3.13", "3.14"]
|
python-version: ["3.9", "3.10", "3.11"]
|
||||||
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"
|
|
||||||
- database: sqlite
|
|
||||||
python-version: "3.12"
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
@@ -83,4 +81,4 @@ jobs:
|
|||||||
file: src/coverage.xml
|
file: src/coverage.xml
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
fail_ci_if_error: false
|
fail_ci_if_error: false
|
||||||
if: matrix.database == 'postgres' && matrix.python-version == '3.13'
|
if: matrix.database == 'postgres' && matrix.python-version == '3.11'
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
This file is part of pretix (Community Edition).
|
This file is part of pretix (Community Edition).
|
||||||
|
|
||||||
Copyright (C) 2014-2020 Raphael Michel and contributors
|
Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
Copyright (C) 2020-today pretix GmbH and contributors
|
Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
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.
|
Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.13-trixie
|
FROM python:3.11-bookworm
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ pretix
|
|||||||
:target: https://docs.pretix.eu/
|
:target: https://docs.pretix.eu/
|
||||||
|
|
||||||
.. image:: https://github.com/pretix/pretix/workflows/Tests/badge.svg
|
.. image:: https://github.com/pretix/pretix/workflows/Tests/badge.svg
|
||||||
:target: https://github.com/pretix/pretix/actions/workflows/tests.yml
|
|
||||||
|
|
||||||
.. image:: https://codecov.io/gh/pretix/pretix/branch/master/graph/badge.svg
|
.. image:: https://codecov.io/gh/pretix/pretix/branch/master/graph/badge.svg
|
||||||
:target: https://codecov.io/gh/pretix/pretix
|
:target: https://codecov.io/gh/pretix/pretix
|
||||||
|
|||||||
Vendored
+95
-79
@@ -6,14 +6,10 @@
|
|||||||
{%- 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>
|
||||||
<html class="writer-html5" lang="{{ lang_attr }}"{% if sphinx_version_info >= (7, 2) %} data-content_root="{{ content_root }}"{% endif %}>
|
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
|
||||||
|
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
{{ metatags }}
|
{{ metatags }}
|
||||||
@@ -22,50 +18,59 @@
|
|||||||
<title>{{ title|striptags|e }}{{ titlesuffix }}</title>
|
<title>{{ title|striptags|e }}{{ titlesuffix }}</title>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{#- CSS #}
|
|
||||||
{%- for css_file in css_files %}
|
{#- CSS #}
|
||||||
{%- if css_file|attr("filename") %}
|
{%- for css in css_files %}
|
||||||
{{ css_tag(css_file) }}
|
{%- if css|attr("rel") %}
|
||||||
|
<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_file, 1)|escape }}" type="text/css" />
|
<link rel="stylesheet" href="{{ pathto(css, 1) }}" type="text/css" />
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
|
||||||
{#- FAVICON #}
|
{%- for cssfile in extra_css_files %}
|
||||||
{%- if favicon_url %}
|
<link rel="stylesheet" href="{{ pathto(cssfile, 1) }}" type="text/css" />
|
||||||
<link rel="shortcut icon" href="{{ favicon_url }}"/>
|
{%- endfor -%}
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
{#- CANONICAL URL (deprecated) #}
|
{#- FAVICON
|
||||||
{%- if theme_canonical_url and not pageurl %}
|
favicon_url is the only context var necessary since Sphinx 4.
|
||||||
|
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 not embedded %}
|
<!--[if lt IE 9]>
|
||||||
{%- for scriptfile in script_files %}
|
<script src="{{ pathto('_static/js/html5shiv.min.js', 1) }}"></script>
|
||||||
{{ js_tag(scriptfile) }}
|
<![endif]-->
|
||||||
{%- endfor %}
|
{%- if not embedded %}
|
||||||
|
{# 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') %}
|
||||||
@@ -118,23 +123,23 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{%- block navigation %}
|
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
|
||||||
{#- Translators: This is an ARIA section label for the main navigation menu -#}
|
{% block menu %}
|
||||||
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="{{ _('Navigation menu') }}">
|
{#
|
||||||
{%- block menu %}
|
The singlehtml builder doesn't handle this toctree call when the
|
||||||
{%- set toctree = toctree(maxdepth=theme_navigation_depth|int,
|
toctree is empty. Skip building this for now.
|
||||||
collapse=theme_collapse_navigation|tobool,
|
#}
|
||||||
includehidden=theme_includehidden|tobool,
|
{% if 'singlehtml' not in builder %}
|
||||||
titles_only=theme_titles_only|tobool) %}
|
{% set global_toc = toctree(maxdepth=theme_navigation_depth|int, collapse=theme_collapse_navigation, includehidden=True) %}
|
||||||
{%- if toctree %}
|
{% endif %}
|
||||||
{{ toctree }}
|
{% if global_toc %}
|
||||||
{%- else %}
|
{{ global_toc }}
|
||||||
|
{% 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 %}
|
||||||
@@ -153,42 +158,53 @@
|
|||||||
<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" aria-label="{{ _('Mobile navigation menu') }}" {% if theme_style_nav_header_background %} style="background: {{theme_style_nav_header_background}}" {% endif %}>
|
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
|
||||||
{%- 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(master_doc) }}">{{ project }}</a>
|
<a href="{{ pathto('index') }}">{{ project }}</a>
|
||||||
{%- endblock %}
|
{% endblock %}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="wy-nav-content">
|
|
||||||
{%- block content %}
|
{# PAGE CONTENT #}
|
||||||
{%- if theme_style_external_links|tobool %}
|
<div class="wy-nav-content">
|
||||||
<div class="rst-content style-external-links">
|
<div class="rst-content">
|
||||||
{%- else %}
|
{% include "breadcrumbs.html" %}
|
||||||
<div class="rst-content">
|
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
|
||||||
{%- endif %}
|
<div itemprop="articleBody" class="section">
|
||||||
{% include "breadcrumbs.html" %}
|
{% block body %}{% endblock %}
|
||||||
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
|
</div>
|
||||||
{%- block document %}
|
<div class="articleComments">
|
||||||
<div itemprop="articleBody">
|
{% block comments %}{% endblock %}
|
||||||
{% block body %}{% endblock %}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{%- if self.comments()|trim %}
|
{% include "footer.html" %}
|
||||||
<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>
|
||||||
@@ -198,7 +214,7 @@
|
|||||||
{% if theme_sticky_navigation %}
|
{% if theme_sticky_navigation %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
jQuery(function () {
|
jQuery(function () {
|
||||||
SphinxRtdTheme.Navigation.enable({{ 'true' if theme_sticky_navigation|tobool else 'false' }});
|
SphinxRtdTheme.StickyNav.enable();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
+161
-179
@@ -1,86 +1,136 @@
|
|||||||
{# 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('_', '-')) %}
|
|
||||||
|
|
||||||
{# Build sphinx_version_info tuple from sphinx_version string in pure Jinja #}
|
{%- macro relbar() %}
|
||||||
{%- set (_ver_major, _ver_minor) = (sphinx_version.split('.') | list)[:2] | map('int') -%}
|
<div class="related">
|
||||||
{%- set sphinx_version_info = (_ver_major, _ver_minor, -1) -%}
|
<h3>{{ _('Navigation') }}</h3>
|
||||||
|
<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 %}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
{%- macro sidebar() %}
|
||||||
<html class="writer-html5" lang="{{ lang_attr }}"{% if sphinx_version_info >= (7, 2) %} data-content_root="{{ content_root }}"{% endif %}>
|
{%- if render_sidebar %}
|
||||||
<head>
|
<div class="sphinxsidebar">
|
||||||
<meta charset="utf-8" />
|
<div class="sphinxsidebarwrapper">
|
||||||
{%- if READTHEDOCS and not embedded %}
|
{%- block sidebarlogo %}
|
||||||
<meta name="readthedocs-addons-api-version" content="1">
|
{%- if logo %}
|
||||||
{%- endif %}
|
<p class="logo"><a href="{{ pathto(master_doc) }}">
|
||||||
{{- metatags }}
|
<img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
</a></p>
|
||||||
{%- block htmltitle %}
|
{%- endif %}
|
||||||
<title>{{ title|striptags|e }}{{ titlesuffix }}</title>
|
{%- endblock %}
|
||||||
{%- endblock -%}
|
{%- if sidebars != None %}
|
||||||
|
{#- 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 %}
|
||||||
|
|
||||||
{#- CSS #}
|
{%- macro script() %}
|
||||||
{%- for css_file in css_files %}
|
<script type="text/javascript">
|
||||||
{%- if css_file|attr("filename") %}
|
var DOCUMENTATION_OPTIONS = {
|
||||||
{{ css_tag(css_file) }}
|
URL_ROOT: '{{ url_root }}',
|
||||||
{%- else %}
|
VERSION: '{{ release|e }}',
|
||||||
<link rel="stylesheet" href="{{ pathto(css_file, 1)|escape }}" type="text/css" />
|
COLLAPSE_INDEX: false,
|
||||||
{%- endif %}
|
FILE_SUFFIX: '{{ '' if no_search_suffix else file_suffix }}',
|
||||||
{%- endfor %}
|
HAS_SOURCE: {{ has_source|lower }},
|
||||||
|
SOURCELINK_SUFFIX: '{{ sourcelink_suffix }}'
|
||||||
{#
|
};
|
||||||
"extra_css_files" is an undocumented Read the Docs theme specific option.
|
</script>
|
||||||
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 %}
|
||||||
{{ js_tag(scriptfile) }}
|
<script type="text/javascript" src="{{ pathto(scriptfile, 1) }}"></script>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
<script src="{{ pathto('_static/js/theme.js', 1) }}"></script>
|
{%- endmacro %}
|
||||||
|
|
||||||
{%- if READTHEDOCS or DEBUG %}
|
{%- macro css() %}
|
||||||
<script src="{{ pathto('_static/js/versions.js', 1) }}"></script>
|
<link rel="stylesheet" href="{{ pathto('_static/' + style, 1) }}" type="text/css" />
|
||||||
{%- endif %}
|
<link rel="stylesheet" href="{{ pathto('_static/pygments.css', 1) }}" type="text/css" />
|
||||||
|
{%- for cssfile in css_files %}
|
||||||
|
<link rel="stylesheet" href="{{ pathto(cssfile, 1) }}" type="text/css" />
|
||||||
|
{%- endfor %}
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
{#- OPENSEARCH #}
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<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 %}
|
||||||
{%- endif %}
|
{%- if favicon %}
|
||||||
{%- endblock %}
|
<link rel="shortcut icon" href="{{ pathto('_static/' + favicon, 1) }}"/>
|
||||||
|
{%- endif %}
|
||||||
{%- block linktags %}
|
{%- if theme_canonical_url %}
|
||||||
|
<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 %}
|
||||||
@@ -93,135 +143,67 @@
|
|||||||
{%- 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 %}
|
{%- endblock %}
|
||||||
{%- block extrahead %} {% endblock %}
|
{%- block extrahead %} {% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
<body>
|
||||||
|
{%- block header %}{% endblock %}
|
||||||
|
|
||||||
<body class="wy-body-for-nav">
|
{%- block relbar1 %}{{ relbar() }}{% endblock %}
|
||||||
|
|
||||||
{%- block extrabody %} {% endblock %}
|
{%- block content %}
|
||||||
<div class="wy-grid-for-nav">
|
{%- block sidebar1 %} {# possible location for sidebar #} {% endblock %}
|
||||||
{#- 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 #}
|
<div class="document">
|
||||||
{# the master_doc variable was renamed to root_doc in Sphinx 4 (master_doc still exists in later Sphinx versions) #}
|
{%- block document %}
|
||||||
{%- set _logo_url = logo_url|default(pathto('_static/' + (logo or ""), 1)) %}
|
<div class="documentwrapper">
|
||||||
{%- set _root_doc = root_doc|default(master_doc) %}
|
{%- if render_sidebar %}
|
||||||
<a href="{{ pathto(_root_doc) }}"{% if not theme_logo_only %} class="icon icon-home"{% endif %}>
|
<div class="bodywrapper">
|
||||||
{% if not theme_logo_only %}{{ project }}{% endif %}
|
{%- endif %}
|
||||||
{%- if logo or logo_url %}
|
<div class="body">
|
||||||
<img src="{{ _logo_url }}" class="logo" alt="{{ _('Logo') }}"/>
|
{% block body %} {% endblock %}
|
||||||
{%- 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>
|
</div>
|
||||||
{%- endblock %}
|
{%- if render_sidebar %}
|
||||||
{% include "footer.html" %}
|
|
||||||
</div>
|
</div>
|
||||||
{%- endblock %}
|
{%- endif %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
{%- endblock %}
|
||||||
</div>
|
|
||||||
{% include "versions.html" -%}
|
|
||||||
|
|
||||||
<script>
|
{%- block sidebar2 %}{{ sidebar() }}{% endblock %}
|
||||||
jQuery(function () {
|
<div class="clearer"></div>
|
||||||
SphinxRtdTheme.Navigation.enable({{ 'true' if theme_sticky_navigation|tobool else 'false' }});
|
</div>
|
||||||
});
|
{%- endblock %}
|
||||||
</script>
|
|
||||||
|
|
||||||
{#- Do not conflict with RTD insertion of analytics script #}
|
{%- block relbar2 %}{{ relbar() }}{% endblock %}
|
||||||
{%- 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 %}
|
||||||
{%- endif %}
|
{%- if last_updated %}
|
||||||
|
{% 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>
|
|
||||||
+19
-6
@@ -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 features such as working with reusable
|
The ``rsa_pubkey`` is optional any only required for certain fatures 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
|
||||||
@@ -197,11 +197,10 @@ Permissions & security profiles
|
|||||||
|
|
||||||
Device authentication is currently hardcoded to grant the following permissions:
|
Device authentication is currently hardcoded to grant the following permissions:
|
||||||
|
|
||||||
* Read event meta data and products etc.
|
* View event meta data and products etc.
|
||||||
* Read and write orders
|
* View orders
|
||||||
* Read and write gift cards
|
* Change orders
|
||||||
* Read and write reusable media
|
* Manage gift cards
|
||||||
* Read vouchers
|
|
||||||
|
|
||||||
Devices cannot change events or products and cannot access vouchers.
|
Devices cannot change events or products and cannot access vouchers.
|
||||||
|
|
||||||
@@ -209,6 +208,20 @@ Additionally, when creating a device through the user interface or API, a user c
|
|||||||
the device. These include an allow list of specific API calls that may be made by the device. pretix ships with security
|
the device. These include an allow list of specific API calls that may be made by the device. pretix ships with security
|
||||||
policies for official pretix apps like pretixSCAN and pretixPOS.
|
policies for official pretix apps like pretixSCAN and pretixPOS.
|
||||||
|
|
||||||
|
Removing a device
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
If you want implement a way to to deprovision a device in your software, you can call the ``revoke`` endpoint to
|
||||||
|
invalidate your API key. There is no way to reverse this operation.
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/device/revoke HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Authorization: Device 1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd
|
||||||
|
|
||||||
|
This can also be done by the user through the web interface.
|
||||||
|
|
||||||
Event selection
|
Event selection
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
|||||||
@@ -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 any object has changed in the meantime, you will receive back a full list
|
``If-Modified-Since`` header. If the 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.
|
||||||
|
|
||||||
|
|||||||
@@ -359,156 +359,3 @@ Performing a ticket search
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer or check-in list does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer or check-in list does not exist **or** you have no permission to view this resource.
|
||||||
:statuscode 404: The requested check-in list does not exist.
|
:statuscode 404: The requested check-in list does not exist.
|
||||||
|
|
||||||
.. _`rest-checkin-annul`:
|
|
||||||
|
|
||||||
Annulment of a check-in
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/checkinrpc/annul/
|
|
||||||
|
|
||||||
If a check-in was made in error and the person was not let in, it can be annulled. We do not recommend this to be used
|
|
||||||
in case of manual check-ins or user interfaces because it is too prone for human errors. It is mostly intended for
|
|
||||||
automated entry systems like a turnstile or automated door, where the check-in is first created, then the door is
|
|
||||||
opened, and then the check-in may be annulled if the system knows that the turnstile did not turn or was out of
|
|
||||||
order.
|
|
||||||
|
|
||||||
This endpoint supports passing multiple check-in lists for the context of a multi-event scan. However, each
|
|
||||||
check-in list passed needs to be from a distinct event.
|
|
||||||
|
|
||||||
Check-ins created by a device can only be annulled by the same device. The datetime of annulment may not be more than
|
|
||||||
15 minutes after the datetime of check-in (value subject to change).
|
|
||||||
|
|
||||||
A status code of 404 is returned if no check-in was found for the given nonce. A status code of 400 is returned when
|
|
||||||
multiple check-ins match the nonce, the input is invalid in another way, the annulment is made from the wrong device,
|
|
||||||
the check-in is already in an annulled or failed state, or the datetime constraint is not valid.
|
|
||||||
|
|
||||||
:<json string nonce: ``nonce`` value of the original check-in.
|
|
||||||
:<json array lists: List of check-in list IDs to search on. No two check-in lists may be from the same event.
|
|
||||||
:<json datetime datetime: Specifies the client-side datetime of the annulment. If not supplied, the current time will be used.
|
|
||||||
:<json string error_explanation: A human-readable description of why the check-in was annulled (optional).
|
|
||||||
:>json string status: ``"ok"``
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
POST /api/v1/organizers/bigevents/checkinrpc/annul/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
{
|
|
||||||
"lists": [1],
|
|
||||||
"nonce": "Pvrk50vUzQd0DhdpNRL4I4OcXsvg70uA",
|
|
||||||
"error_explanation": "Turnstile did not turn"
|
|
||||||
}
|
|
||||||
|
|
||||||
**Example successful response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"status": "ok",
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 400: Invalid or incomplete request, see above
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
|
||||||
:statuscode 404: The requested nonce does not exist.
|
|
||||||
|
|
||||||
|
|
||||||
Check-in history
|
|
||||||
----------------
|
|
||||||
|
|
||||||
.. rst-class:: rest-resource-table
|
|
||||||
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
Field Type Description
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
id integer Internal ID of the check-in
|
|
||||||
successful boolean Whether the check-in was successful
|
|
||||||
error_reason string Category of reason why the check-in was unsuccessful. Currently
|
|
||||||
``"canceled"``, ``"invalid"``, ``"unpaid"`` ``"product"``,
|
|
||||||
``"rules"``, ``"revoked"``, ``"incomplete"``, ``"already_redeemed"``,
|
|
||||||
``"ambiguous"``, ``"error"``, ``"blocked"``, ``"unapproved"``,
|
|
||||||
``"invalid_time"``, ``"annulled"`` or ``null``
|
|
||||||
error_explanation string Additional, human-readable reason for the check-in to be unsuccessful (or ``null``)
|
|
||||||
position integer Internal ID of the order position (or ``null`` for unknown scans)
|
|
||||||
datetime datetime Logical time when the check-in happened
|
|
||||||
created datetime Time when the check-in appeared on the server
|
|
||||||
list integer Internal ID of the check-in list
|
|
||||||
auto_checked_in boolean Whether the check-in was performed by the system automatically
|
|
||||||
gate integer Internal ID of the gate (or ``null``)
|
|
||||||
device integer Internal ID of the device (or ``null``)
|
|
||||||
device_id integer Organizer-internal ID of the device (or ``null``)
|
|
||||||
type string Type of check-in, currently ``"entry"`` or ``"exit"``
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkins/
|
|
||||||
|
|
||||||
Returns a list of all check-in events within a given event.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/events/sampleconf/checkins/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"count": 1,
|
|
||||||
"next": null,
|
|
||||||
"previous": null,
|
|
||||||
"results": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"successful": true,
|
|
||||||
"error_reason": null,
|
|
||||||
"error_explanation": null,
|
|
||||||
"position": 1234,
|
|
||||||
"datetime": "2017-12-25T12:45:23Z",
|
|
||||||
"created": "2017-12-25T12:45:23Z",
|
|
||||||
"list": 2,
|
|
||||||
"auto_checked_in": false,
|
|
||||||
"gate": null,
|
|
||||||
"device": null,
|
|
||||||
"device_id": null,
|
|
||||||
"type": "entry",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
|
||||||
:query datetime created_since: Only return check-ins that have been created since the given date (inclusive).
|
|
||||||
:query datetime created_before: Only return check-ins that have been created before the given date (exclusive).
|
|
||||||
:query datetime datetime_since: Only return check-ins that have happened since the given date (inclusive).
|
|
||||||
:query datetime datetime_before: Only return check-ins that have happened before the given date (exclusive).
|
|
||||||
:query boolean successful: Only return check-ins that have (not) been successful.
|
|
||||||
:query boolean error_reason: Only return check-ins with a specific error reason.
|
|
||||||
:query integer list: Only return check-ins from a specific list.
|
|
||||||
:query string type: Only return check-ins of a specific type.
|
|
||||||
:query integer gate: Only return check-ins from a specific gate.
|
|
||||||
:query integer device: Only return check-ins from a specific device.
|
|
||||||
:query boolean auto_checked_in: Only return check-ins that are (not) auto-checked in.
|
|
||||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``created``,
|
|
||||||
and ``id``.
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
|
||||||
:param event: The ``slug`` field of the event to fetch
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ software_brand string Device software
|
|||||||
software_version string Device software version (read-only)
|
software_version string Device software version (read-only)
|
||||||
created datetime Creation time
|
created datetime Creation time
|
||||||
initialized datetime Time of initialization (or ``null``)
|
initialized datetime Time of initialization (or ``null``)
|
||||||
initialization_token string Token for initialization (field invisible without write permission)
|
initialization_token string Token for initialization
|
||||||
revoked boolean Whether this device no longer has access
|
revoked boolean Whether this device no longer has access
|
||||||
security_profile string The name of a supported security profile restricting API access
|
security_profile string The name of a supported security profile restricting API access
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ Endpoints
|
|||||||
|
|
||||||
Returns a list of all events within a given organizer the authenticated user/token has access to.
|
Returns a list of all events within a given organizer the authenticated user/token has access to.
|
||||||
|
|
||||||
|
Permission required: "Can change event settings"
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -159,6 +161,8 @@ Endpoints
|
|||||||
|
|
||||||
Returns information on one event, identified by its slug.
|
Returns information on one event, identified by its slug.
|
||||||
|
|
||||||
|
Permission required: "Can change event settings"
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -230,6 +234,8 @@ Endpoints
|
|||||||
Please note that events cannot be created as 'live' using this endpoint. Quotas and payment must be added to the
|
Please note that events cannot be created as 'live' using this endpoint. Quotas and payment must be added to the
|
||||||
event before sales can go live.
|
event before sales can go live.
|
||||||
|
|
||||||
|
Permission required: "Can create events"
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -332,6 +338,8 @@ Endpoints
|
|||||||
Please note that you can only copy from events under the same organizer this way. Use the ``clone_from`` parameter
|
Please note that you can only copy from events under the same organizer this way. Use the ``clone_from`` parameter
|
||||||
when creating a new event for this instead.
|
when creating a new event for this instead.
|
||||||
|
|
||||||
|
Permission required: "Can create events"
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -416,15 +424,17 @@ Endpoints
|
|||||||
:param organizer: The ``slug`` field of the organizer of the event to create.
|
:param organizer: The ``slug`` field of the organizer of the event to create.
|
||||||
:param event: The ``slug`` field of the event to copy settings and items from.
|
:param event: The ``slug`` field of the event to copy settings and items from.
|
||||||
:statuscode 201: no error
|
:statuscode 201: no error
|
||||||
:statuscode 400: The event could not be updated due to invalid submitted data.
|
:statuscode 400: The event could not be created due to invalid submitted data.
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to update this resource.
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||||
|
|
||||||
|
|
||||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/
|
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/
|
||||||
|
|
||||||
Updates an event
|
Updates an event
|
||||||
|
|
||||||
|
Permission required: "Can change event settings"
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -500,6 +510,8 @@ Endpoints
|
|||||||
|
|
||||||
Delete an event. Note that events with orders cannot be deleted to ensure data integrity.
|
Delete an event. Note that events with orders cannot be deleted to ensure data integrity.
|
||||||
|
|
||||||
|
Permission required: "Can change event settings"
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -549,6 +561,8 @@ organizer level.
|
|||||||
|
|
||||||
Get current values of event settings.
|
Get current values of event settings.
|
||||||
|
|
||||||
|
Permission required: "Can change event settings" (Exception: with device auth, *some* settings can always be *read*.)
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -601,8 +615,6 @@ organizer level.
|
|||||||
|
|
||||||
Updates event settings. Note that ``PUT`` is not allowed here, only ``PATCH``.
|
Updates event settings. Note that ``PUT`` is not allowed here, only ``PATCH``.
|
||||||
|
|
||||||
Permission "Can change event settings" is always required. Some keys require additional permissions.
|
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
Settings can be stored at different levels in pretix. If a value is not set on event level, a default setting
|
Settings can be stored at different levels in pretix. If a value is not set on event level, a default setting
|
||||||
|
|||||||
@@ -349,45 +349,6 @@ Endpoints
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer or event or exhibitor does not exist **or** you have no permission to view it.
|
:statuscode 403: The requested organizer or event or exhibitor does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/(id)/vouchers/bulk_attach/
|
|
||||||
|
|
||||||
Attaches many **existing** vouchers to an exhibitor. You need to send either the ``id`` **or** the ``code`` field of
|
|
||||||
the voucher, but you need to send the same field for all entries.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
POST /api/v1/organizers/bigevents/events/sampleconf/exhibitors/1/vouchers/bulk_attach/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": 15,
|
|
||||||
"exhibitor_comment": "Free ticket"
|
|
||||||
},
|
|
||||||
..
|
|
||||||
]
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of a valid organizer
|
|
||||||
:param event: The ``slug`` field of the event to use
|
|
||||||
:param id: The ``id`` field of the exhibitor to use
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 400: Invalid data sent, e.g. voucher does not exist
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer or event or exhibitor does not exist **or** you have no permission to view it.
|
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/
|
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/exhibitors/
|
||||||
|
|
||||||
Create a new exhibitor.
|
Create a new exhibitor.
|
||||||
|
|||||||
@@ -19,14 +19,12 @@ at :ref:`plugin-docs`.
|
|||||||
item_bundles
|
item_bundles
|
||||||
item_add-ons
|
item_add-ons
|
||||||
item_meta_properties
|
item_meta_properties
|
||||||
item_program_times
|
|
||||||
questions
|
questions
|
||||||
question_options
|
question_options
|
||||||
quotas
|
quotas
|
||||||
seats
|
seats
|
||||||
orders
|
orders
|
||||||
invoices
|
invoices
|
||||||
transactions
|
|
||||||
vouchers
|
vouchers
|
||||||
discounts
|
discounts
|
||||||
checkin
|
checkin
|
||||||
@@ -56,7 +54,6 @@ at :ref:`plugin-docs`.
|
|||||||
digital
|
digital
|
||||||
exhibitors
|
exhibitors
|
||||||
imported_secrets
|
imported_secrets
|
||||||
offlinesales
|
|
||||||
shipping
|
shipping
|
||||||
billing_invoices
|
billing_invoices
|
||||||
billing_var
|
billing_var
|
||||||
|
|||||||
+13
-175
@@ -1,5 +1,3 @@
|
|||||||
.. _rest-invoices:
|
|
||||||
|
|
||||||
Invoices
|
Invoices
|
||||||
========
|
========
|
||||||
|
|
||||||
@@ -22,13 +20,10 @@ invoice_from_name string Sender address:
|
|||||||
invoice_from string Sender address: Address lines
|
invoice_from string Sender address: Address lines
|
||||||
invoice_from_zipcode string Sender address: ZIP code
|
invoice_from_zipcode string Sender address: ZIP code
|
||||||
invoice_from_city string Sender address: City
|
invoice_from_city string Sender address: City
|
||||||
invoice_from_state string Sender address: State (only used in some countries)
|
|
||||||
invoice_from_country string Sender address: Country code
|
invoice_from_country string Sender address: Country code
|
||||||
invoice_from_tax_id string Sender address: Local Tax ID
|
invoice_from_tax_id string Sender address: Local Tax ID
|
||||||
invoice_from_vat_id string Sender address: EU VAT ID
|
invoice_from_vat_id string Sender address: EU VAT ID
|
||||||
invoice_to string Full recipient address
|
invoice_to string Full recipient address
|
||||||
invoice_to_is_business boolean Recipient address: Business vs individual (``null`` for
|
|
||||||
invoices created before pretix 2025.6).
|
|
||||||
invoice_to_company string Recipient address: Company name
|
invoice_to_company string Recipient address: Company name
|
||||||
invoice_to_name string Recipient address: Person name
|
invoice_to_name string Recipient address: Person name
|
||||||
invoice_to_street string Recipient address: Address lines
|
invoice_to_street string Recipient address: Address lines
|
||||||
@@ -38,7 +33,6 @@ invoice_to_state string Recipient addre
|
|||||||
invoice_to_country string Recipient address: Country code
|
invoice_to_country string Recipient address: Country code
|
||||||
invoice_to_vat_id string Recipient address: EU VAT ID
|
invoice_to_vat_id string Recipient address: EU VAT ID
|
||||||
invoice_to_beneficiary string Invoice beneficiary
|
invoice_to_beneficiary string Invoice beneficiary
|
||||||
invoice_to_transmission_info object Additional transmission info (see :ref:`rest-transmission-types`)
|
|
||||||
custom_field string Custom invoice address field
|
custom_field string Custom invoice address field
|
||||||
date date Invoice date
|
date date Invoice date
|
||||||
refers string Invoice number of an invoice this invoice refers to
|
refers string Invoice number of an invoice this invoice refers to
|
||||||
@@ -81,12 +75,17 @@ lines list of objects The actual invo
|
|||||||
for all invoice lines
|
for all invoice lines
|
||||||
created before this field was introduced as well as for
|
created before this field was introduced as well as for
|
||||||
all lines not created by a fee (e.g. a product).
|
all lines not created by a fee (e.g. a product).
|
||||||
├ period_start datetime Start date of the service or delivery period of the invoice line.
|
├ event_date_from datetime Start date of the (sub)event this line was created for as it
|
||||||
Can be ``null`` if not known.
|
was set during invoice creation. Can be ``null`` for all invoice
|
||||||
├ period_end datetime End date of the service or delivery period of the invoice line.
|
lines created before this was introduced as well as for lines in
|
||||||
Can be ``null`` if not known.
|
an event series not created by a product (e.g. shipping or
|
||||||
├ event_date_from datetime Deprecated alias of ``period_start``.
|
cancellation fees).
|
||||||
├ event_date_to datetime Deprecated alias of ``period_end``.
|
├ event_date_to datetime End date of the (sub)event this line was created for as it
|
||||||
|
was set during invoice creation. Can be ``null`` for all invoice
|
||||||
|
lines created before this was introduced as well as for lines in
|
||||||
|
an event series not created by a product (e.g. shipping or
|
||||||
|
cancellation fees) as well as whenever the respective (sub)event
|
||||||
|
has no end date set.
|
||||||
├ event_location string Location of the (sub)event this line was created for as it
|
├ event_location string Location of the (sub)event this line was created for as it
|
||||||
was set during invoice creation. Can be ``null`` for all invoice
|
was set during invoice creation. Can be ``null`` for all invoice
|
||||||
lines created before this was introduced as well as for lines in
|
lines created before this was introduced as well as for lines in
|
||||||
@@ -109,12 +108,6 @@ foreign_currency_rate decimal (string) If ``foreign_cu
|
|||||||
foreign_currency_rate_date date If ``foreign_currency_rate`` is set, this signifies the
|
foreign_currency_rate_date date If ``foreign_currency_rate`` is set, this signifies the
|
||||||
date at which the currency rate was obtained.
|
date at which the currency rate was obtained.
|
||||||
internal_reference string Customer's reference to be printed on the invoice.
|
internal_reference string Customer's reference to be printed on the invoice.
|
||||||
transmission_type string Requested transmission channel (see :ref:`rest-transmission-types`)
|
|
||||||
transmission_provider string Selected transmission provider (depends on installed
|
|
||||||
plugins). ``null`` if not yet chosen.
|
|
||||||
transmission_status string Transmission status, one of ``unknown`` (pre-2025.6),
|
|
||||||
``pending``, ``inflight``, ``failed``, and ``completed``.
|
|
||||||
transmission_date datetime Time of last change in transmission status (may be ``null``).
|
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -126,76 +119,6 @@ transmission_date datetime Time of last ch
|
|||||||
|
|
||||||
The ``tax_code`` attribute has been added.
|
The ``tax_code`` attribute has been added.
|
||||||
|
|
||||||
.. versionchanged:: 2025.6
|
|
||||||
|
|
||||||
The attributes ``invoice_to_is_business``, ``invoice_to_transmission_info``, ``transmission_type``,
|
|
||||||
``transmission_provider``, ``transmission_status``, and ``transmission_date`` have been added.
|
|
||||||
|
|
||||||
|
|
||||||
.. _`rest-transmission-types`:
|
|
||||||
|
|
||||||
Transmission types
|
|
||||||
------------------
|
|
||||||
|
|
||||||
pretix supports multiple ways to transmit an invoice from the organizer to the invoice recipient.
|
|
||||||
For each transmission type, different fields are supported in the ``transmission_info`` object of the
|
|
||||||
invoice address. Currently, pretix supports the following transmission types:
|
|
||||||
|
|
||||||
Email
|
|
||||||
"""""
|
|
||||||
|
|
||||||
The identifier ``"email"`` represents the transmission of PDF invoices through email.
|
|
||||||
This is the default transmission type in pretix and has some special behavior for backwards compatibility.
|
|
||||||
Transmission is always executed through the provider ``"email_pdf"``.
|
|
||||||
The ``transmission_info`` object may contain the following properties:
|
|
||||||
|
|
||||||
.. rst-class:: rest-resource-table
|
|
||||||
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
Field Type Description
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
transmission_email_address string Optional. An email address other than the order address
|
|
||||||
that the invoice should be sent to.
|
|
||||||
Business customers only.
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
|
|
||||||
Peppol
|
|
||||||
""""""
|
|
||||||
|
|
||||||
The identifier ``"peppol"`` represents the transmission of XML invoices through the `Peppol`_ network.
|
|
||||||
This is only available for business addresses.
|
|
||||||
This is not supported by pretix out of the box and requires the use of a suitable plugin.
|
|
||||||
The ``transmission_info`` object may contain the following properties:
|
|
||||||
|
|
||||||
.. rst-class:: rest-resource-table
|
|
||||||
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
Field Type Description
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
transmission_peppol_participant_id string Required. The Peppol participant ID of the recipient.
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
|
|
||||||
Italian Exchange System
|
|
||||||
"""""""""""""""""""""""
|
|
||||||
|
|
||||||
The identifier ``"it_sdi"`` represents the transmission of XML invoices through the `Sistema di Interscambio`_ network used in Italy.
|
|
||||||
This is only available for addresses with country ``"IT"``.
|
|
||||||
This is not supported by pretix out of the box and requires the use of a suitable plugin.
|
|
||||||
The ``transmission_info`` object may contain the following properties:
|
|
||||||
|
|
||||||
.. rst-class:: rest-resource-table
|
|
||||||
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
Field Type Description
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
transmission_it_sdi_codice_fiscale string Required for non-business address. Fiscal code of the
|
|
||||||
recipient.
|
|
||||||
transmission_it_sdi_pec string Required for business addresses. Address for certified
|
|
||||||
electronic mail.
|
|
||||||
transmission_it_sdi_recipient_code string Required for businesses. SdI recipient code.
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
|
|
||||||
If this type is selected, ``vat_id`` is required for business addresses.
|
|
||||||
|
|
||||||
List of all invoices
|
List of all invoices
|
||||||
--------------------
|
--------------------
|
||||||
@@ -234,13 +157,11 @@ List of all invoices
|
|||||||
"invoice_from": "Demo street 12",
|
"invoice_from": "Demo street 12",
|
||||||
"invoice_from_zipcode":"",
|
"invoice_from_zipcode":"",
|
||||||
"invoice_from_city":"Demo town",
|
"invoice_from_city":"Demo town",
|
||||||
"invoice_from_state":"CA",
|
|
||||||
"invoice_from_country":"US",
|
"invoice_from_country":"US",
|
||||||
"invoice_from_tax_id":"",
|
"invoice_from_tax_id":"",
|
||||||
"invoice_from_vat_id":"",
|
"invoice_from_vat_id":"",
|
||||||
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
|
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
|
||||||
"invoice_to_company": "Sample company",
|
"invoice_to_company": "Sample company",
|
||||||
"invoice_to_is_business": true,
|
|
||||||
"invoice_to_name": "John Doe",
|
"invoice_to_name": "John Doe",
|
||||||
"invoice_to_street": "Test street 12",
|
"invoice_to_street": "Test street 12",
|
||||||
"invoice_to_zipcode": "12345",
|
"invoice_to_zipcode": "12345",
|
||||||
@@ -249,7 +170,6 @@ List of all invoices
|
|||||||
"invoice_to_country": "TE",
|
"invoice_to_country": "TE",
|
||||||
"invoice_to_vat_id": "EU123456789",
|
"invoice_to_vat_id": "EU123456789",
|
||||||
"invoice_to_beneficiary": "",
|
"invoice_to_beneficiary": "",
|
||||||
"invoice_to_transmission_info": {},
|
|
||||||
"custom_field": null,
|
"custom_field": null,
|
||||||
"date": "2017-12-01",
|
"date": "2017-12-01",
|
||||||
"refers": null,
|
"refers": null,
|
||||||
@@ -271,8 +191,6 @@ List of all invoices
|
|||||||
"fee_internal_type": null,
|
"fee_internal_type": null,
|
||||||
"event_date_from": "2017-12-27T10:00:00Z",
|
"event_date_from": "2017-12-27T10:00:00Z",
|
||||||
"event_date_to": null,
|
"event_date_to": null,
|
||||||
"period_start": "2017-12-27T10:00:00Z",
|
|
||||||
"period_end": "2017-12-27T10:00:00Z",
|
|
||||||
"event_location": "Heidelberg",
|
"event_location": "Heidelberg",
|
||||||
"attendee_name": null,
|
"attendee_name": null,
|
||||||
"gross_value": "23.00",
|
"gross_value": "23.00",
|
||||||
@@ -284,11 +202,7 @@ List of all invoices
|
|||||||
],
|
],
|
||||||
"foreign_currency_display": "PLN",
|
"foreign_currency_display": "PLN",
|
||||||
"foreign_currency_rate": "4.2408",
|
"foreign_currency_rate": "4.2408",
|
||||||
"foreign_currency_rate_date": "2017-07-24",
|
"foreign_currency_rate_date": "2017-07-24"
|
||||||
"transmission_type": "email",
|
|
||||||
"transmission_provider": "email_pdf",
|
|
||||||
"transmission_status": "completed",
|
|
||||||
"transmission_date": "2017-07-24T10:00:00Z"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -383,13 +297,11 @@ Fetching individual invoices
|
|||||||
"invoice_from": "Demo street 12",
|
"invoice_from": "Demo street 12",
|
||||||
"invoice_from_zipcode":"",
|
"invoice_from_zipcode":"",
|
||||||
"invoice_from_city":"Demo town",
|
"invoice_from_city":"Demo town",
|
||||||
"invoice_from_state":"CA",
|
|
||||||
"invoice_from_country":"US",
|
"invoice_from_country":"US",
|
||||||
"invoice_from_tax_id":"",
|
"invoice_from_tax_id":"",
|
||||||
"invoice_from_vat_id":"",
|
"invoice_from_vat_id":"",
|
||||||
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
|
"invoice_to": "Sample company\nJohn Doe\nTest street 12\n12345 Testington\nTestikistan\nVAT-ID: EU123456789",
|
||||||
"invoice_to_company": "Sample company",
|
"invoice_to_company": "Sample company",
|
||||||
"invoice_to_is_business": true,
|
|
||||||
"invoice_to_name": "John Doe",
|
"invoice_to_name": "John Doe",
|
||||||
"invoice_to_street": "Test street 12",
|
"invoice_to_street": "Test street 12",
|
||||||
"invoice_to_zipcode": "12345",
|
"invoice_to_zipcode": "12345",
|
||||||
@@ -398,7 +310,6 @@ Fetching individual invoices
|
|||||||
"invoice_to_country": "TE",
|
"invoice_to_country": "TE",
|
||||||
"invoice_to_vat_id": "EU123456789",
|
"invoice_to_vat_id": "EU123456789",
|
||||||
"invoice_to_beneficiary": "",
|
"invoice_to_beneficiary": "",
|
||||||
"invoice_to_transmission_info": {},
|
|
||||||
"custom_field": null,
|
"custom_field": null,
|
||||||
"date": "2017-12-01",
|
"date": "2017-12-01",
|
||||||
"refers": null,
|
"refers": null,
|
||||||
@@ -420,8 +331,6 @@ Fetching individual invoices
|
|||||||
"fee_internal_type": null,
|
"fee_internal_type": null,
|
||||||
"event_date_from": "2017-12-27T10:00:00Z",
|
"event_date_from": "2017-12-27T10:00:00Z",
|
||||||
"event_date_to": null,
|
"event_date_to": null,
|
||||||
"period_start": "2017-12-27T10:00:00Z",
|
|
||||||
"period_end": "2017-12-27T10:00:00Z",
|
|
||||||
"event_location": "Heidelberg",
|
"event_location": "Heidelberg",
|
||||||
"attendee_name": null,
|
"attendee_name": null,
|
||||||
"gross_value": "23.00",
|
"gross_value": "23.00",
|
||||||
@@ -433,11 +342,7 @@ Fetching individual invoices
|
|||||||
],
|
],
|
||||||
"foreign_currency_display": "PLN",
|
"foreign_currency_display": "PLN",
|
||||||
"foreign_currency_rate": "4.2408",
|
"foreign_currency_rate": "4.2408",
|
||||||
"foreign_currency_rate_date": "2017-07-24",
|
"foreign_currency_rate_date": "2017-07-24"
|
||||||
"transmission_type": "email",
|
|
||||||
"transmission_provider": "email_pdf",
|
|
||||||
"transmission_status": "completed",
|
|
||||||
"transmission_date": "2017-07-24T10:00:00Z"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -542,70 +447,3 @@ Invoices cannot be edited directly, but the following actions can be triggered:
|
|||||||
:statuscode 400: The invoice has already been canceled
|
:statuscode 400: The invoice has already been canceled
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
||||||
|
|
||||||
|
|
||||||
Transmitting invoices
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
Invoices are transmitted automatically when created during order creation or payment receipt,
|
|
||||||
but in other cases transmission may need to be triggered manually.
|
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/transmit/
|
|
||||||
|
|
||||||
Transmits the invoice to the recipient, but only if it is in ``pending`` state.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/events/sampleconf/invoices/00001/transmit/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 204 No Content
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/pdf
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
|
||||||
:param event: The ``slug`` field of the event to fetch
|
|
||||||
:param number: The ``number`` field of the invoice to transmit
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to transmit this invoice **or** the invoice may not be transmitted
|
|
||||||
:statuscode 409: The invoice is currently in transmission
|
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/retransmit/
|
|
||||||
|
|
||||||
Transmits the invoice to the recipient even if transmission was already attempted previously.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/events/sampleconf/invoices/00001/retransmit/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 204 No Content
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/pdf
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
|
||||||
:param event: The ``slug`` field of the event to fetch
|
|
||||||
:param number: The ``number`` field of the invoice to transmit
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to transmit this invoice **or** the invoice may not be transmitted
|
|
||||||
:statuscode 409: The invoice is currently in transmission
|
|
||||||
|
|
||||||
|
|
||||||
.. _Peppol: https://en.wikipedia.org/wiki/PEPPOL
|
|
||||||
.. _Sistema di Interscambio: https://it.wikipedia.org/wiki/Fattura_elettronica_in_Italia
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
Item program times
|
|
||||||
==================
|
|
||||||
|
|
||||||
Resource description
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
Program times for products (items) that can be set in addition to event times, e.g. to display seperate schedules within an event.
|
|
||||||
Note that ``program_times`` are not available for items inside event series.
|
|
||||||
The program times resource contains the following public fields:
|
|
||||||
|
|
||||||
.. rst-class:: rest-resource-table
|
|
||||||
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
Field Type Description
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
id integer Internal ID of the program time
|
|
||||||
start datetime The start date time for this program time slot.
|
|
||||||
end datetime The end date time for this program time slot.
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
|
|
||||||
.. versionchanged:: TODO
|
|
||||||
|
|
||||||
The resource has been added.
|
|
||||||
|
|
||||||
|
|
||||||
Endpoints
|
|
||||||
---------
|
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/program_times/
|
|
||||||
|
|
||||||
Returns a list of all program times for a given item.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/events/sampleconf/items/11/program_times/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"count": 3,
|
|
||||||
"next": null,
|
|
||||||
"previous": null,
|
|
||||||
"results": [
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"start": "2025-08-14T22:00:00Z",
|
|
||||||
"end": "2025-08-15T00:00:00Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"start": "2025-08-12T22:00:00Z",
|
|
||||||
"end": "2025-08-13T22:00:00Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 14,
|
|
||||||
"start": "2025-08-15T22:00:00Z",
|
|
||||||
"end": "2025-08-17T22:00:00Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
|
||||||
:param event: The ``slug`` field of the event to fetch
|
|
||||||
:param item: The ``id`` field of the item to fetch
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event/item does not exist **or** you have no permission to view this resource.
|
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/program_times/(id)/
|
|
||||||
|
|
||||||
Returns information on one program time, identified by its ID.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/events/sampleconf/items/1/program_times/1/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"start": "2025-08-15T22:00:00Z",
|
|
||||||
"end": "2025-10-27T23:00:00Z"
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
|
||||||
:param event: The ``slug`` field of the event to fetch
|
|
||||||
:param item: The ``id`` field of the item to fetch
|
|
||||||
:param id: The ``id`` field of the program time to fetch
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/program_times/
|
|
||||||
|
|
||||||
Creates a new program time
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
POST /api/v1/organizers/bigevents/events/sampleconf/items/1/program_times/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"start": "2025-08-15T10:00:00Z",
|
|
||||||
"end": "2025-08-15T22:00:00Z"
|
|
||||||
}
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 201 Created
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"id": 17,
|
|
||||||
"start": "2025-08-15T10:00:00Z",
|
|
||||||
"end": "2025-08-15T22:00:00Z"
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event/item to create a program time for
|
|
||||||
:param event: The ``slug`` field of the event to create a program time for
|
|
||||||
:param item: The ``id`` field of the item to create a program time for
|
|
||||||
:statuscode 201: no error
|
|
||||||
:statuscode 400: The program time could not be created due to invalid submitted data.
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
|
|
||||||
|
|
||||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/program_times/(id)/
|
|
||||||
|
|
||||||
Update a program time. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
|
||||||
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
|
||||||
want to change.
|
|
||||||
|
|
||||||
You can change all fields of the resource except the ``id`` field.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
PATCH /api/v1/organizers/bigevents/events/sampleconf/items/1/program_times/1/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
Content-Type: application/json
|
|
||||||
Content-Length: 94
|
|
||||||
|
|
||||||
{
|
|
||||||
"start": "2025-08-14T10:00:00Z"
|
|
||||||
}
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"start": "2025-08-14T10:00:00Z",
|
|
||||||
"end": "2025-08-15T12:00:00Z"
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
|
||||||
:param event: The ``slug`` field of the event to modify
|
|
||||||
:param id: The ``id`` field of the item to modify
|
|
||||||
:param id: The ``id`` field of the program time to modify
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 400: The program time could not be modified due to invalid submitted data
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
|
|
||||||
|
|
||||||
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/items/(id)/program_times/(id)/
|
|
||||||
|
|
||||||
Delete a program time.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
DELETE /api/v1/organizers/bigevents/events/sampleconf/items/1/program_times/1/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 204 No Content
|
|
||||||
Vary: Accept
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
|
||||||
:param event: The ``slug`` field of the event to modify
|
|
||||||
:param id: The ``id`` field of the item to modify
|
|
||||||
:param id: The ``id`` field of the program time to delete
|
|
||||||
:statuscode 204: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
|
|
||||||
+10
-36
@@ -139,10 +139,6 @@ has_variations boolean Shows whether
|
|||||||
variations list of objects A list with one object for each variation of this item.
|
variations list of objects A list with one object for each variation of this item.
|
||||||
Can be empty. Only writable during creation,
|
Can be empty. Only writable during creation,
|
||||||
use separate endpoint to modify this later.
|
use separate endpoint to modify this later.
|
||||||
program_times list of objects A list with one object for each program time of this item.
|
|
||||||
Can be empty. Only writable during creation,
|
|
||||||
use separate endpoint to modify this later.
|
|
||||||
Not available for items in event series.
|
|
||||||
├ id integer Internal ID of the variation
|
├ id integer Internal ID of the variation
|
||||||
├ value multi-lingual string The "name" of the variation
|
├ value multi-lingual string The "name" of the variation
|
||||||
├ default_price money (string) The price set directly for this variation or ``null``
|
├ default_price money (string) The price set directly for this variation or ``null``
|
||||||
@@ -229,10 +225,6 @@ meta_data object Values set fo
|
|||||||
|
|
||||||
The ``hidden_if_item_available_mode`` attributes has been added.
|
The ``hidden_if_item_available_mode`` attributes has been added.
|
||||||
|
|
||||||
.. versionchanged:: 2025.9
|
|
||||||
|
|
||||||
The ``program_times`` attribute has been added.
|
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
-----
|
-----
|
||||||
|
|
||||||
@@ -240,11 +232,9 @@ Please note that an item either always has variations or never has. Once created
|
|||||||
change to an item without and vice versa. To create an item with variations ensure that you POST an item with at least
|
change to an item without and vice versa. To create an item with variations ensure that you POST an item with at least
|
||||||
one variation.
|
one variation.
|
||||||
|
|
||||||
Also note that ``variations``, ``bundles``, ``addons`` and ``program_times`` are only supported on ``POST``. To update/delete variations,
|
Also note that ``variations``, ``bundles``, and ``addons`` are only supported on ``POST``. To update/delete variations,
|
||||||
bundles, add-ons and program times please use the dedicated nested endpoints. By design this endpoint does not support ``PATCH`` and ``PUT``
|
bundles, and add-ons please use the dedicated nested endpoints. By design this endpoint does not support ``PATCH`` and ``PUT``
|
||||||
with nested ``variations``, ``bundles``, ``addons`` and/or ``program_times``.
|
with nested ``variations``, ``bundles`` and/or ``addons``.
|
||||||
|
|
||||||
``program_times`` is not available to items in event series.
|
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
@@ -383,8 +373,7 @@ Endpoints
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"addons": [],
|
"addons": [],
|
||||||
"bundles": [],
|
"bundles": []
|
||||||
"program_times": []
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -536,8 +525,7 @@ Endpoints
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"addons": [],
|
"addons": [],
|
||||||
"bundles": [],
|
"bundles": []
|
||||||
"program_times": []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -665,13 +653,7 @@ Endpoints
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"addons": [],
|
"addons": [],
|
||||||
"bundles": [],
|
"bundles": []
|
||||||
"program_times": [
|
|
||||||
{
|
|
||||||
"start": "2025-08-14T22:00:00Z",
|
|
||||||
"end": "2025-08-15T00:00:00Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -791,13 +773,7 @@ Endpoints
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"addons": [],
|
"addons": [],
|
||||||
"bundles": [],
|
"bundles": []
|
||||||
"program_times": [
|
|
||||||
{
|
|
||||||
"start": "2025-08-14T22:00:00Z",
|
|
||||||
"end": "2025-08-15T00:00:00Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event to create an item for
|
:param organizer: The ``slug`` field of the organizer of the event to create an item for
|
||||||
@@ -813,9 +789,8 @@ Endpoints
|
|||||||
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||||
want to change.
|
want to change.
|
||||||
|
|
||||||
You can change all fields of the resource except the ``has_variations``, ``variations``, ``addon`` and the
|
You can change all fields of the resource except the ``has_variations``, ``variations`` and the ``addon`` field. If
|
||||||
``program_times`` field. If you need to update/delete variations, add-ons or program times, please use the nested
|
you need to update/delete variations or add-ons please use the nested dedicated endpoints.
|
||||||
dedicated endpoints.
|
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@@ -949,8 +924,7 @@ Endpoints
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"addons": [],
|
"addons": [],
|
||||||
"bundles": [],
|
"bundles": []
|
||||||
"program_times": []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
|||||||
@@ -1,219 +0,0 @@
|
|||||||
Offline sales
|
|
||||||
=============
|
|
||||||
|
|
||||||
.. note:: This API is only available when the plugin **pretix-offlinesales** is installed (pretix Hosted and Enterprise only).
|
|
||||||
|
|
||||||
The offline sales module allows you to create batches of tickets intended for the sale outside the system.
|
|
||||||
|
|
||||||
Resource description
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
The offline sales batch resource contains the following public fields:
|
|
||||||
|
|
||||||
.. rst-class:: rest-resource-table
|
|
||||||
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
Field Type Description
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
id integer Internal batch ID
|
|
||||||
creation datetime Time of creation
|
|
||||||
testmode boolean ``true`` if orders are created in test mode
|
|
||||||
sales_channel string Sales channel of the orders
|
|
||||||
layout integer Internal ID of the chosen ticket layout
|
|
||||||
subevent integer Internal ID of the chosen subevent (or ``null``)
|
|
||||||
item integer Internal ID of the chosen product
|
|
||||||
variation integer Internal ID of the chosen variation (or ``null``)
|
|
||||||
amount integer Number of tickets in the batch
|
|
||||||
comment string Internal comment
|
|
||||||
orders list of strings List of order codes (omitted in list view for performance reasons)
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
|
|
||||||
Endpoints
|
|
||||||
---------
|
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/offlinesalesbatches/
|
|
||||||
|
|
||||||
Returns a list of all offline sales batches
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/events/democon/offlinesalesbatches/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: text/javascript
|
|
||||||
|
|
||||||
{
|
|
||||||
"count": 1,
|
|
||||||
"next": null,
|
|
||||||
"previous": null,
|
|
||||||
"results": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"creation": "2025-07-08T18:27:32.134368+02:00",
|
|
||||||
"testmode": False,
|
|
||||||
"sales_channel": "web",
|
|
||||||
"comment": "Batch for sale at the event",
|
|
||||||
"layout": 3,
|
|
||||||
"subevent": null,
|
|
||||||
"item": 23,
|
|
||||||
"variation": null,
|
|
||||||
"amount": 7
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
:query page: The page number in case of a multi-page result set, default is 1
|
|
||||||
:param organizer: The ``slug`` field of a valid organizer
|
|
||||||
:param event: The ``slug`` field of a valid event
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/offlinesalesbatches/(id)/
|
|
||||||
|
|
||||||
Returns information on a given batch.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/events/democon/offlinesalesbatches/1/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: text/javascript
|
|
||||||
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"creation": "2025-07-08T18:27:32.134368+02:00",
|
|
||||||
"testmode": False,
|
|
||||||
"sales_channel": "web",
|
|
||||||
"comment": "Batch for sale at the event",
|
|
||||||
"layout": 3,
|
|
||||||
"subevent": null,
|
|
||||||
"item": 23,
|
|
||||||
"variation": null,
|
|
||||||
"amount": 7,
|
|
||||||
"orders": ["TSRNN", "3FBSL", "WMDNJ", "BHW9H", "MXSUG", "DSDAP", "URLLE"]
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
|
||||||
:param event: The ``slug`` field of the event to fetch
|
|
||||||
:param id: The ``id`` field of the batch to fetch
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
|
||||||
|
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/offlinesalesbatches/
|
|
||||||
|
|
||||||
With this API call, you can instruct the system to create a new batch.
|
|
||||||
|
|
||||||
Since batches can contain up to 10,000 tickets, they are created asynchronously on the server.
|
|
||||||
If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
|
|
||||||
The body points you to the check URL of the result. Running a ``GET`` request on that result URL will
|
|
||||||
yield one of the following status codes:
|
|
||||||
|
|
||||||
* ``200 OK`` – The creation of the batch has succeeded. The body will be your resulting batch with the same information as in the detail endpoint above.
|
|
||||||
* ``409 Conflict`` – Your creation job is still running. The body will be JSON with the structure ``{"status": "running"}``. ``status`` can be ``waiting`` before the task is actually being processed. Please retry, but wait at least one second before you do.
|
|
||||||
* ``410 Gone`` – Creating the batch has failed permanently (e.g. quota no longer available). The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
|
|
||||||
* ``404 Not Found`` – The job does not exist / is expired.
|
|
||||||
|
|
||||||
.. note:: To avoid performance issues, a maximum amount of 10000 is currently allowed.
|
|
||||||
|
|
||||||
.. note:: Do not wait multiple hours or more to retrieve your result. After a longer wait time, ``409`` might be returned permanently due to technical constraints, even though nothing will happen any more.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
POST /api/v1/organizers/bigevents/events/sampleconf/offlinesalesbatches/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"testmode": True,
|
|
||||||
"layout": 123,
|
|
||||||
"item": 14,
|
|
||||||
"sales_channel": "web",
|
|
||||||
"amount": 10,
|
|
||||||
}
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"check": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/offlinesalesbatches/check/29891ede-196f-4942-9e26-d055a36e98b8/"
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
|
||||||
:param event: The ``slug`` field of the event to fetch
|
|
||||||
:statuscode 202: no error
|
|
||||||
:statuscode 400: Invalid input options
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
|
||||||
|
|
||||||
|
|
||||||
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/offlinesalesbatches/(id)/render/
|
|
||||||
|
|
||||||
With this API call, you can render the PDF representation of a batch.
|
|
||||||
|
|
||||||
Since batches can contain up to 10,000 tickets, they are rendered asynchronously on the server.
|
|
||||||
If your input parameters validate correctly, a ``202 Accepted`` status code is returned.
|
|
||||||
The body points you to the download URL of the result. Running a ``GET`` request on that result URL will
|
|
||||||
yield one of the following status codes:
|
|
||||||
|
|
||||||
* ``200 OK`` – The creation of the batch has succeeded. The body will be your resulting batch with the same information as in the detail endpoint above.
|
|
||||||
* ``409 Conflict`` – Your rendering process is still running. The body will be JSON with the structure ``{"status": "running"}``. ``status`` can be ``waiting`` before the task is actually being processed. Please retry, but wait at least one second before you do.
|
|
||||||
* ``410 Gone`` – Rendering the batch has failed permanently. The body will be JSON with the structure ``{"status": "failed", "message": "Error message"}``
|
|
||||||
* ``404 Not Found`` – The rendering job does not exist / is expired.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
POST /api/v1/organizers/bigevents/events/sampleconf/offlinesalesbatches/1/render HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"download": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/offlinesalesbatches/1/download/29891ede-196f-4942-9e26-d055a36e98b8/3f279f13-c198-4137-b49b-9b360ce9fcce/"
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
|
||||||
:param event: The ``slug`` field of the event to fetch
|
|
||||||
:param id: The ``id`` field of the batch to fetch
|
|
||||||
:statuscode 202: no error
|
|
||||||
:statuscode 400: Invalid input options
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
|
||||||
|
|
||||||
@@ -41,7 +41,6 @@ expires datetime The order will
|
|||||||
payment_date date **DEPRECATED AND INACCURATE** Date of payment receipt
|
payment_date date **DEPRECATED AND INACCURATE** Date of payment receipt
|
||||||
payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order
|
payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order
|
||||||
total money (string) Total value of this order
|
total money (string) Total value of this order
|
||||||
tax_rounding_mode string Tax rounding mode, see :ref:`algorithms-rounding`
|
|
||||||
comment string Internal comment on this order
|
comment string Internal comment on this order
|
||||||
api_meta object Meta data for that order. Only available through API, no guarantees
|
api_meta object Meta data for that order. Only available through API, no guarantees
|
||||||
on the content structure. You can use this to save references to your system.
|
on the content structure. You can use this to save references to your system.
|
||||||
@@ -66,16 +65,11 @@ invoice_address object Invoice address
|
|||||||
├ state string Customer state (ISO 3166-2 code). Only supported in
|
├ state string Customer state (ISO 3166-2 code). Only supported in
|
||||||
AU, BR, CA, CN, MY, MX, and US.
|
AU, BR, CA, CN, MY, MX, and US.
|
||||||
├ internal_reference string Customer's internal reference to be printed on the invoice
|
├ internal_reference string Customer's internal reference to be printed on the invoice
|
||||||
|
|
||||||
├ custom_field string Custom invoice address field
|
├ custom_field string Custom invoice address field
|
||||||
├ vat_id string Customer VAT ID
|
├ vat_id string Customer VAT ID
|
||||||
├ vat_id_validated string ``true``, if the VAT ID has been validated against the
|
└ vat_id_validated string ``true``, if the VAT ID has been validated against the
|
||||||
EU VAT service and validation was successful. This only
|
EU VAT service and validation was successful. This only
|
||||||
happens in rare cases.
|
happens in rare cases.
|
||||||
├ transmission_type string Transmission channel for invoice (see also :ref:`rest-transmission-types`).
|
|
||||||
Defaults to ``email``.
|
|
||||||
└ transmission_info object Transmission-channel specific information (or ``null``).
|
|
||||||
See also :ref:`rest-transmission-types`.
|
|
||||||
positions list of objects List of order positions (see below). By default, only
|
positions list of objects List of order positions (see below). By default, only
|
||||||
non-canceled positions are included.
|
non-canceled positions are included.
|
||||||
fees list of objects List of fees included in the order total. By default, only
|
fees list of objects List of fees included in the order total. By default, only
|
||||||
@@ -117,8 +111,6 @@ cancellation_date datetime Time of order c
|
|||||||
reliable for orders that have been cancelled,
|
reliable for orders that have been cancelled,
|
||||||
reactivated and cancelled again.
|
reactivated and cancelled again.
|
||||||
plugin_data object Additional data added by plugins.
|
plugin_data object Additional data added by plugins.
|
||||||
use_gift_cards list of strings List of unique gift card secrets that are used to pay
|
|
||||||
for this order.
|
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -150,18 +142,6 @@ use_gift_cards list of strings List of unique
|
|||||||
|
|
||||||
The ``plugin_data`` attribute has been added.
|
The ``plugin_data`` attribute has been added.
|
||||||
|
|
||||||
.. versionchanged:: 2025.6
|
|
||||||
|
|
||||||
The ``invoice_address.transmission_type`` and ``invoice_address.transmission_info`` attributes have been added.
|
|
||||||
|
|
||||||
.. versionchanged:: 2025.10
|
|
||||||
|
|
||||||
The ``tax_rounding_mode`` attribute has been added.
|
|
||||||
|
|
||||||
.. versionchanged:: 2026.03
|
|
||||||
|
|
||||||
The ``use_gift_cards`` attribute has been added.
|
|
||||||
|
|
||||||
.. _order-position-resource:
|
.. _order-position-resource:
|
||||||
|
|
||||||
Order position resource
|
Order position resource
|
||||||
@@ -369,7 +349,6 @@ List of all orders
|
|||||||
"payment_provider": "banktransfer",
|
"payment_provider": "banktransfer",
|
||||||
"fees": [],
|
"fees": [],
|
||||||
"total": "23.00",
|
"total": "23.00",
|
||||||
"tax_rounding_mode": "line",
|
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"custom_followup_at": null,
|
"custom_followup_at": null,
|
||||||
"checkin_attention": false,
|
"checkin_attention": false,
|
||||||
@@ -389,9 +368,7 @@ List of all orders
|
|||||||
"state": "",
|
"state": "",
|
||||||
"internal_reference": "",
|
"internal_reference": "",
|
||||||
"vat_id": "EU123456789",
|
"vat_id": "EU123456789",
|
||||||
"vat_id_validated": false,
|
"vat_id_validated": false
|
||||||
"transmission_type": "email",
|
|
||||||
"transmission_info": {}
|
|
||||||
},
|
},
|
||||||
"positions": [
|
"positions": [
|
||||||
{
|
{
|
||||||
@@ -430,7 +407,6 @@ List of all orders
|
|||||||
"seat": null,
|
"seat": null,
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
"id": 1337,
|
|
||||||
"list": 44,
|
"list": 44,
|
||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
@@ -614,7 +590,6 @@ Fetching individual orders
|
|||||||
"payment_provider": "banktransfer",
|
"payment_provider": "banktransfer",
|
||||||
"fees": [],
|
"fees": [],
|
||||||
"total": "23.00",
|
"total": "23.00",
|
||||||
"tax_rounding_mode": "line",
|
|
||||||
"comment": "",
|
"comment": "",
|
||||||
"api_meta": {},
|
"api_meta": {},
|
||||||
"custom_followup_at": null,
|
"custom_followup_at": null,
|
||||||
@@ -635,9 +610,7 @@ Fetching individual orders
|
|||||||
"state": "",
|
"state": "",
|
||||||
"internal_reference": "",
|
"internal_reference": "",
|
||||||
"vat_id": "EU123456789",
|
"vat_id": "EU123456789",
|
||||||
"vat_id_validated": false,
|
"vat_id_validated": false
|
||||||
"transmission_type": "email",
|
|
||||||
"transmission_info": {}
|
|
||||||
},
|
},
|
||||||
"positions": [
|
"positions": [
|
||||||
{
|
{
|
||||||
@@ -676,7 +649,6 @@ Fetching individual orders
|
|||||||
"seat": null,
|
"seat": null,
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
"id": 1337,
|
|
||||||
"list": 44,
|
"list": 44,
|
||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
@@ -993,6 +965,8 @@ Creating orders
|
|||||||
|
|
||||||
* does not support file upload questions
|
* does not support file upload questions
|
||||||
|
|
||||||
|
* does not support redeeming gift cards
|
||||||
|
|
||||||
* does not support or validate memberships
|
* does not support or validate memberships
|
||||||
|
|
||||||
|
|
||||||
@@ -1022,7 +996,6 @@ Creating orders
|
|||||||
provider will not be called to do anything about this (i.e. if you pass a bank account to a debit provider, *no*
|
provider will not be called to do anything about this (i.e. if you pass a bank account to a debit provider, *no*
|
||||||
charge will be created), this is just informative in case you *handled the payment already*.
|
charge will be created), this is just informative in case you *handled the payment already*.
|
||||||
* ``payment_date`` (optional) – Date and time of the completion of the payment.
|
* ``payment_date`` (optional) – Date and time of the completion of the payment.
|
||||||
* ``tax_rounding_mode`` (optional)
|
|
||||||
* ``comment`` (optional)
|
* ``comment`` (optional)
|
||||||
* ``custom_followup_at`` (optional)
|
* ``custom_followup_at`` (optional)
|
||||||
* ``checkin_attention`` (optional)
|
* ``checkin_attention`` (optional)
|
||||||
@@ -1042,10 +1015,8 @@ Creating orders
|
|||||||
* ``internal_reference``
|
* ``internal_reference``
|
||||||
* ``vat_id``
|
* ``vat_id``
|
||||||
* ``vat_id_validated`` (optional) – If you need support for reverse charge (rarely the case), you need to check
|
* ``vat_id_validated`` (optional) – If you need support for reverse charge (rarely the case), you need to check
|
||||||
yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
|
yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
|
||||||
trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
|
trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
|
||||||
* ``transmission_type`` (optional, defaults to ``email``)
|
|
||||||
* ``transmission_info`` (optional, see also :ref:`rest-transmission-types`)
|
|
||||||
|
|
||||||
* ``positions``
|
* ``positions``
|
||||||
|
|
||||||
@@ -1070,7 +1041,6 @@ Creating orders
|
|||||||
* ``valid_until`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
|
* ``valid_until`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
|
||||||
* ``requested_valid_from`` (optional, can be set **instead** of ``valid_from`` and ``valid_until`` to signal a user choice for the start time that may or may not be respected)
|
* ``requested_valid_from`` (optional, can be set **instead** of ``valid_from`` and ``valid_until`` to signal a user choice for the start time that may or may not be respected)
|
||||||
* ``use_reusable_medium`` (optional, causes the new ticket to take over the given reusable medium, identified by its ID)
|
* ``use_reusable_medium`` (optional, causes the new ticket to take over the given reusable medium, identified by its ID)
|
||||||
* ``discount`` (optional, only possible if ``price`` is set; attention: if this is set to not-``null`` on any position, automatic calculation of discounts will not run)
|
|
||||||
* ``answers``
|
* ``answers``
|
||||||
|
|
||||||
* ``question``
|
* ``question``
|
||||||
@@ -1099,14 +1069,6 @@ Creating orders
|
|||||||
whether these emails are enabled for certain sales channels. If set to ``null``, behavior will be controlled by pretix'
|
whether these emails are enabled for certain sales channels. If set to ``null``, behavior will be controlled by pretix'
|
||||||
settings based on the sales channels (added in pretix 4.7). Defaults to ``false``.
|
settings based on the sales channels (added in pretix 4.7). Defaults to ``false``.
|
||||||
Used to be ``send_mail`` before pretix 3.14.
|
Used to be ``send_mail`` before pretix 3.14.
|
||||||
* ``use_gift_cards`` (optional) The provided gift cards will be used to pay for this order. They will be debited and
|
|
||||||
all the necessary payment records for these transactions will be created. The gift cards will be used in sequence to
|
|
||||||
pay for the order. Processing of the gift cards stops as soon as the order is payed for. All gift card transactions
|
|
||||||
are listed under ``payments`` in the response.
|
|
||||||
This option can only be used with orders that are in the pending state.
|
|
||||||
The ``use_gift_cards`` attribute can not be combined with ``payment_info`` and ``payment_provider`` fields. If the
|
|
||||||
order isn't completely paid after its creation with ``use_gift_cards``, then a subsequent request to the payment
|
|
||||||
endpoint is needed.
|
|
||||||
|
|
||||||
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
|
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
|
||||||
to incrementing integers starting with ``1``. Then, you can reference one of these
|
to incrementing integers starting with ``1``. Then, you can reference one of these
|
||||||
@@ -1655,7 +1617,6 @@ List of all order positions
|
|||||||
"blocked": null,
|
"blocked": null,
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
"id": 1337,
|
|
||||||
"list": 44,
|
"list": 44,
|
||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
@@ -1731,56 +1692,6 @@ List of all order positions
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/orderpositions/
|
|
||||||
|
|
||||||
Returns a list of all order positions within all events of a given organizer (with sufficient access permissions).
|
|
||||||
|
|
||||||
The supported query parameters and output format of this endpoint are almost identical to those of the list endpoint
|
|
||||||
within an event.
|
|
||||||
The only changes are that responses also contain the ``event`` attribute in each result and that the 'pdf_data'
|
|
||||||
parameter is not supported.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/orderpositions/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
X-Page-Generated: 2017-12-01T10:00:00Z
|
|
||||||
|
|
||||||
{
|
|
||||||
"count": 1,
|
|
||||||
"next": null,
|
|
||||||
"previous": null,
|
|
||||||
"results": [
|
|
||||||
{
|
|
||||||
"id:": 23442
|
|
||||||
"event": "sampleconf",
|
|
||||||
"order": "ABC12",
|
|
||||||
"positionid": 1,
|
|
||||||
"canceled": false,
|
|
||||||
"item": 1345,
|
|
||||||
...
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Fetching individual positions
|
Fetching individual positions
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
||||||
@@ -1834,7 +1745,6 @@ Fetching individual positions
|
|||||||
"seat": null,
|
"seat": null,
|
||||||
"checkins": [
|
"checkins": [
|
||||||
{
|
{
|
||||||
"id": 1337,
|
|
||||||
"list": 44,
|
"list": 44,
|
||||||
"type": "entry",
|
"type": "entry",
|
||||||
"gate": null,
|
"gate": null,
|
||||||
@@ -2016,7 +1926,6 @@ Manipulating individual positions
|
|||||||
|
|
||||||
(Full order position resource, see above.)
|
(Full order position resource, see above.)
|
||||||
|
|
||||||
:query boolean check_quotas: Whether to check quotas before committing item changes, default is ``true``
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event
|
:param organizer: The ``slug`` field of the organizer of the event
|
||||||
:param event: The ``slug`` field of the event
|
:param event: The ``slug`` field of the event
|
||||||
:param id: The ``id`` field of the order position to update
|
:param id: The ``id`` field of the order position to update
|
||||||
@@ -2096,7 +2005,6 @@ Manipulating individual positions
|
|||||||
|
|
||||||
(Full order position resource, see above.)
|
(Full order position resource, see above.)
|
||||||
|
|
||||||
:query boolean check_quotas: Whether to check quotas before creating the new position, default is ``true``
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event
|
:param organizer: The ``slug`` field of the organizer of the event
|
||||||
:param event: The ``slug`` field of the event
|
:param event: The ``slug`` field of the event
|
||||||
|
|
||||||
@@ -2383,7 +2291,6 @@ otherwise, such as splitting an order or changing fees.
|
|||||||
|
|
||||||
(Full order position resource, see above.)
|
(Full order position resource, see above.)
|
||||||
|
|
||||||
:query boolean check_quotas: Whether to check quotas before patching or creating positions, default is ``true``
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event
|
:param organizer: The ``slug`` field of the organizer of the event
|
||||||
:param event: The ``slug`` field of the event
|
:param event: The ``slug`` field of the event
|
||||||
:param code: The ``code`` field of the order to update
|
:param code: The ``code`` field of the order to update
|
||||||
@@ -2579,7 +2486,6 @@ Order payment endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"amount": "23.00",
|
"amount": "23.00",
|
||||||
"comment": "Overpayment",
|
|
||||||
"mark_canceled": false
|
"mark_canceled": false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,6 @@ name string The organizer's
|
|||||||
slug string A short form of the name, used e.g. in URLs.
|
slug string A short form of the name, used e.g. in URLs.
|
||||||
public_url string The public, customer-facing URL of the organizer, where
|
public_url string The public, customer-facing URL of the organizer, where
|
||||||
the list of all events can be found (read-only).
|
the list of all events can be found (read-only).
|
||||||
plugins list A list of package names of the enabled plugins for this
|
|
||||||
organizer. Note that most plugins are enabled on the
|
|
||||||
event level (or both levels). If you remove a plugin
|
|
||||||
that is also enabled on some events, it will
|
|
||||||
automatically be removed from all events as well.
|
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -58,10 +53,7 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"name": "Big Events LLC",
|
"name": "Big Events LLC",
|
||||||
"slug": "Big Events",
|
"slug": "Big Events",
|
||||||
"public_url": "https://pretix.eu/bigevents/",
|
"public_url": "https://pretix.eu/bigevents/"
|
||||||
"plugins": [
|
|
||||||
"pretix_datev"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -95,10 +87,7 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"name": "Big Events LLC",
|
"name": "Big Events LLC",
|
||||||
"slug": "Big Events",
|
"slug": "Big Events",
|
||||||
"public_url": "https://pretix.eu/bigevents/",
|
"public_url": "https://pretix.eu/bigevents/"
|
||||||
"plugins": [
|
|
||||||
"pretix_datev"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -106,48 +95,6 @@ Endpoints
|
|||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||||
|
|
||||||
.. http:patch:: /api/v1/organizers/(organizer)/
|
|
||||||
|
|
||||||
Updates an organizer. Currently only the ``plugins`` field may be updated.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
PATCH /api/v1/organizers/bigevents/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"plugins": [
|
|
||||||
"pretix_seating"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "Big Events LLC",
|
|
||||||
"slug": "Big Events",
|
|
||||||
"public_url": "https://pretix.eu/bigevents/",
|
|
||||||
"plugins": [
|
|
||||||
"pretix_seating"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to update
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 400: The organizer could not be updated due to invalid submitted data.
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to update this resource.
|
|
||||||
|
|
||||||
Organizer settings
|
Organizer settings
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
@@ -170,6 +117,8 @@ information about the properties.
|
|||||||
|
|
||||||
Get current values of organizer settings.
|
Get current values of organizer settings.
|
||||||
|
|
||||||
|
Permission required: "Can change organizer settings"
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ closed boolean Whether the quo
|
|||||||
field).
|
field).
|
||||||
release_after_exit boolean Whether the quota regains capacity as soon as some tickets
|
release_after_exit boolean Whether the quota regains capacity as soon as some tickets
|
||||||
have been scanned at an exit.
|
have been scanned at an exit.
|
||||||
ignore_for_event_availability boolean Whether the quota is ignored when calculating the event's
|
|
||||||
availability of tickets.
|
|
||||||
available boolean Whether this quota is available. Only returned if ``with_availability=true``
|
available boolean Whether this quota is available. Only returned if ``with_availability=true``
|
||||||
is set on the request. Do not rely on this value for critical operations, it may be
|
is set on the request. Do not rely on this value for critical operations, it may be
|
||||||
slightly out of date.
|
slightly out of date.
|
||||||
@@ -38,10 +36,6 @@ available_number integer Number of avail
|
|||||||
slightly out of date. ``null`` means unlimited.
|
slightly out of date. ``null`` means unlimited.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
.. versionchanged:: 2025.7
|
|
||||||
|
|
||||||
The attribute ``ignore_for_event_availability`` has been added.
|
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -78,8 +72,7 @@ Endpoints
|
|||||||
"variations": [1, 4, 5, 7],
|
"variations": [1, 4, 5, 7],
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"close_when_sold_out": false,
|
"close_when_sold_out": false,
|
||||||
"closed": false,
|
"closed": false
|
||||||
"ignore_for_event_availability": false
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -125,8 +118,7 @@ Endpoints
|
|||||||
"variations": [1, 4, 5, 7],
|
"variations": [1, 4, 5, 7],
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"close_when_sold_out": false,
|
"close_when_sold_out": false,
|
||||||
"closed": false,
|
"closed": false
|
||||||
"ignore_for_event_availability": false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -157,8 +149,7 @@ Endpoints
|
|||||||
"variations": [1, 4, 5, 7],
|
"variations": [1, 4, 5, 7],
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"close_when_sold_out": false,
|
"close_when_sold_out": false,
|
||||||
"closed": false,
|
"closed": false
|
||||||
"ignore_for_event_availability": false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -177,8 +168,7 @@ Endpoints
|
|||||||
"variations": [1, 4, 5, 7],
|
"variations": [1, 4, 5, 7],
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"close_when_sold_out": false,
|
"close_when_sold_out": false,
|
||||||
"closed": false,
|
"closed": false
|
||||||
"ignore_for_event_availability": false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer of the event/item to create a quota for
|
:param organizer: The ``slug`` field of the organizer of the event/item to create a quota for
|
||||||
@@ -233,8 +223,7 @@ Endpoints
|
|||||||
],
|
],
|
||||||
"subevent": null,
|
"subevent": null,
|
||||||
"close_when_sold_out": false,
|
"close_when_sold_out": false,
|
||||||
"closed": false,
|
"closed": false
|
||||||
"ignore_for_event_availability": false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ Endpoints
|
|||||||
.. http:post:: /api/v1/organizers/(organizer)/reusablemedia/lookup/
|
.. http:post:: /api/v1/organizers/(organizer)/reusablemedia/lookup/
|
||||||
|
|
||||||
Look up a new reusable medium by its identifier. In some cases, this might lead to the automatic creation of a new
|
Look up a new reusable medium by its identifier. In some cases, this might lead to the automatic creation of a new
|
||||||
medium behind the scenes, therefore this endpoint requires write permissions.
|
medium behind the scenes.
|
||||||
|
|
||||||
This endpoint, and this endpoint only, might return media from a different organizer if there is a cross-acceptance
|
This endpoint, and this endpoint only, might return media from a different organizer if there is a cross-acceptance
|
||||||
agreement. In this case, only linked gift cards will be returned, no order position or customer records,
|
agreement. In this case, only linked gift cards will be returned, no order position or customer records,
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ Endpoints
|
|||||||
|
|
||||||
Creates a new subevent.
|
Creates a new subevent.
|
||||||
|
|
||||||
|
Permission required: "Can create events"
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -298,6 +300,8 @@ Endpoints
|
|||||||
provide all fields of the resource, other fields will be reset to default. With ``PATCH``, you only need to provide
|
provide all fields of the resource, other fields will be reset to default. With ``PATCH``, you only need to provide
|
||||||
the fields that you want to change.
|
the fields that you want to change.
|
||||||
|
|
||||||
|
Permission required: "Can change event settings"
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -369,6 +373,8 @@ Endpoints
|
|||||||
|
|
||||||
Delete a sub-event. Note that events with orders cannot be deleted to ensure data integrity.
|
Delete a sub-event. Note that events with orders cannot be deleted to ensure data integrity.
|
||||||
|
|
||||||
|
Permission required: "Can change event settings"
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ rate decimal (string) Tax rate in per
|
|||||||
code string Codified reason for tax rate (or ``null``), see :ref:`rest-taxcodes`.
|
code string Codified reason for tax rate (or ``null``), see :ref:`rest-taxcodes`.
|
||||||
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
|
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
|
||||||
the specified product price
|
the specified product price
|
||||||
default boolean If ``true`` (default), this is the default tax rate for this event
|
|
||||||
(there can only be one per event).
|
|
||||||
eu_reverse_charge boolean **DEPRECATED**. If ``true``, EU reverse charge rules
|
eu_reverse_charge boolean **DEPRECATED**. If ``true``, EU reverse charge rules
|
||||||
are applied. Will be ignored if custom rules are set.
|
are applied. Will be ignored if custom rules are set.
|
||||||
Use custom rules instead.
|
Use custom rules instead.
|
||||||
@@ -50,10 +48,6 @@ custom_rules object Dynamic rules s
|
|||||||
|
|
||||||
The ``code`` attribute has been added.
|
The ``code`` attribute has been added.
|
||||||
|
|
||||||
.. versionchanged:: 2025.4
|
|
||||||
|
|
||||||
The ``default`` attribute has been added.
|
|
||||||
|
|
||||||
.. _rest-taxcodes:
|
.. _rest-taxcodes:
|
||||||
|
|
||||||
Tax codes
|
Tax codes
|
||||||
@@ -117,7 +111,6 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "VAT"},
|
"name": {"en": "VAT"},
|
||||||
"default": true,
|
|
||||||
"internal_name": "VAT",
|
"internal_name": "VAT",
|
||||||
"code": "S/standard",
|
"code": "S/standard",
|
||||||
"rate": "19.00",
|
"rate": "19.00",
|
||||||
@@ -160,7 +153,6 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "VAT"},
|
"name": {"en": "VAT"},
|
||||||
"default": true,
|
|
||||||
"internal_name": "VAT",
|
"internal_name": "VAT",
|
||||||
"code": "S/standard",
|
"code": "S/standard",
|
||||||
"rate": "19.00",
|
"rate": "19.00",
|
||||||
@@ -211,7 +203,6 @@ Endpoints
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": {"en": "VAT"},
|
"name": {"en": "VAT"},
|
||||||
"default": false,
|
|
||||||
"internal_name": "VAT",
|
"internal_name": "VAT",
|
||||||
"code": "S/standard",
|
"code": "S/standard",
|
||||||
"rate": "19.00",
|
"rate": "19.00",
|
||||||
|
|||||||
+15
-72
@@ -24,58 +24,21 @@ all_events boolean Whether this te
|
|||||||
limit_events list List of event slugs this team has access to
|
limit_events list List of event slugs this team has access to
|
||||||
require_2fa boolean Whether members of this team are required to use
|
require_2fa boolean Whether members of this team are required to use
|
||||||
two-factor authentication
|
two-factor authentication
|
||||||
all_event_permissions bool Whether members of this team are granted all event-level
|
can_create_events boolean
|
||||||
permissions, including future additions
|
can_change_teams boolean
|
||||||
limit_event_permissions list of strings The event-level permissions team members are granted
|
can_change_organizer_settings boolean
|
||||||
all_organizer_permissions bool Whether members of this team are granted all organizer-level
|
can_manage_customers boolean
|
||||||
permissions, including future additions
|
can_manage_reusable_media boolean
|
||||||
all_organizer_permissions list of strings The organizer-level permissions team members are granted
|
can_manage_gift_cards boolean
|
||||||
can_create_events boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``.
|
can_change_event_settings boolean
|
||||||
can_change_teams boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``.
|
can_change_items boolean
|
||||||
can_change_organizer_settings boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``.
|
can_view_orders boolean
|
||||||
can_manage_customers boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``.
|
can_change_orders boolean
|
||||||
can_manage_reusable_media boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``.
|
can_view_vouchers boolean
|
||||||
can_manage_gift_cards boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``.
|
can_change_vouchers boolean
|
||||||
can_change_event_settings boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``.
|
can_checkin_orders boolean
|
||||||
can_change_items boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``.
|
|
||||||
can_view_orders boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``.
|
|
||||||
can_change_orders boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``.
|
|
||||||
can_view_vouchers boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``.
|
|
||||||
can_change_vouchers boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``.
|
|
||||||
can_checkin_orders boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``.
|
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
Possible values for ``limit_organizer_permissions`` defined in the core pretix system (plugins might add more)::
|
|
||||||
|
|
||||||
organizer.events:create
|
|
||||||
organizer.settings.general:write
|
|
||||||
organizer.teams:write
|
|
||||||
organizer.seatingplans:write
|
|
||||||
organizer.giftcards:read
|
|
||||||
organizer.giftcards:write
|
|
||||||
organizer.customers:read
|
|
||||||
organizer.customers:write
|
|
||||||
organizer.reusablemedia:read
|
|
||||||
organizer.reusablemedia:write
|
|
||||||
organizer.devices:read
|
|
||||||
organizer.devices:write
|
|
||||||
organizer.outgoingmails:read
|
|
||||||
|
|
||||||
Possible values for ``limit_event_permissions`` defined in the core pretix system (plugins might add more)::
|
|
||||||
|
|
||||||
event.settings.general:write
|
|
||||||
event.settings.payment:write
|
|
||||||
event.settings.tax:write
|
|
||||||
event.settings.invoicing:write
|
|
||||||
event.subevents:write
|
|
||||||
event.items:write
|
|
||||||
event.orders:read
|
|
||||||
event.orders:write
|
|
||||||
event.orders:checkin
|
|
||||||
event.vouchers:read
|
|
||||||
event.vouchers:write
|
|
||||||
event:cancel
|
|
||||||
|
|
||||||
Team member resource
|
Team member resource
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
@@ -158,10 +121,6 @@ Team endpoints
|
|||||||
"all_events": true,
|
"all_events": true,
|
||||||
"limit_events": [],
|
"limit_events": [],
|
||||||
"require_2fa": true,
|
"require_2fa": true,
|
||||||
"all_event_permissions": true,
|
|
||||||
"limit_event_permissions": [],
|
|
||||||
"all_organizer_permissions": true,
|
|
||||||
"limit_organizer_permissions": [],
|
|
||||||
"can_create_events": true,
|
"can_create_events": true,
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
@@ -200,10 +159,6 @@ Team endpoints
|
|||||||
"all_events": true,
|
"all_events": true,
|
||||||
"limit_events": [],
|
"limit_events": [],
|
||||||
"require_2fa": true,
|
"require_2fa": true,
|
||||||
"all_event_permissions": true,
|
|
||||||
"limit_event_permissions": [],
|
|
||||||
"all_organizer_permissions": true,
|
|
||||||
"limit_organizer_permissions": [],
|
|
||||||
"can_create_events": true,
|
"can_create_events": true,
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
@@ -232,10 +187,7 @@ Team endpoints
|
|||||||
"all_events": true,
|
"all_events": true,
|
||||||
"limit_events": [],
|
"limit_events": [],
|
||||||
"require_2fa": true,
|
"require_2fa": true,
|
||||||
"all_event_permissions": true,
|
"can_create_events": true,
|
||||||
"limit_event_permissions": [],
|
|
||||||
"all_organizer_permissions": true,
|
|
||||||
"limit_organizer_permissions": [],
|
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,10 +205,6 @@ Team endpoints
|
|||||||
"all_events": true,
|
"all_events": true,
|
||||||
"limit_events": [],
|
"limit_events": [],
|
||||||
"require_2fa": true,
|
"require_2fa": true,
|
||||||
"all_event_permissions": true,
|
|
||||||
"limit_event_permissions": [],
|
|
||||||
"all_organizer_permissions": true,
|
|
||||||
"limit_organizer_permissions": [],
|
|
||||||
"can_create_events": true,
|
"can_create_events": true,
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
@@ -284,8 +232,7 @@ Team endpoints
|
|||||||
Content-Length: 94
|
Content-Length: 94
|
||||||
|
|
||||||
{
|
{
|
||||||
"all_organizer_permissions": false,
|
"can_create_events": true
|
||||||
"limit_organizer_permissions": ["organizer.events:create"]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -302,10 +249,6 @@ Team endpoints
|
|||||||
"all_events": true,
|
"all_events": true,
|
||||||
"limit_events": [],
|
"limit_events": [],
|
||||||
"require_2fa": true,
|
"require_2fa": true,
|
||||||
"all_event_permissions": true,
|
|
||||||
"limit_event_permissions": [],
|
|
||||||
"all_organizer_permissions": false,
|
|
||||||
"limit_organizer_permissions": ["organizer.events:create"],
|
|
||||||
"can_create_events": true,
|
"can_create_events": true,
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,232 +0,0 @@
|
|||||||
.. _rest-transactions:
|
|
||||||
|
|
||||||
Transactions
|
|
||||||
============
|
|
||||||
|
|
||||||
Transactions are an additional way to think about orders. They are are an immutable, filterable view into an order's
|
|
||||||
history and are a good basis for financial reporting.
|
|
||||||
|
|
||||||
Our financial model
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
You can think of a pretix order similar to a debtor account in double-entry bookkeeping. For example, the flow of an
|
|
||||||
order could look like this:
|
|
||||||
|
|
||||||
===================================================== ==================== =====================
|
|
||||||
Transaction Debit Credit
|
|
||||||
===================================================== ==================== =====================
|
|
||||||
Order is placed with two tickets € 500
|
|
||||||
Order is paid partially with a gift card € 200
|
|
||||||
Remainder is paid with a credit card € 300
|
|
||||||
One of the tickets is canceled **-** € 250
|
|
||||||
Refund is made to the credit card **-** € 250
|
|
||||||
**Balance** **€ 250** **€ 250**
|
|
||||||
===================================================== ==================== =====================
|
|
||||||
|
|
||||||
If an order is fully settled, the sums of both columns match. However, as the movements in both columns do not always
|
|
||||||
happen at the same time, at some times during the lifecycle of an order the sums are not balanced, in which case we
|
|
||||||
consider an order to be "pending payment" or "overpaid".
|
|
||||||
|
|
||||||
In the API, the "Debit" column is represented by the "transaction" resource listed on this page.
|
|
||||||
In many cases, the left column *usually* also matches the data returned by the :ref:`rest-invoices` resource, but there
|
|
||||||
are two important differences:
|
|
||||||
|
|
||||||
- pretix may be configured such that an invoice is not always generated for an order. In this case, only the transactions
|
|
||||||
return the full data set.
|
|
||||||
|
|
||||||
- pretix does not enforce a new invoice to be created e.g. when a ticket is changed to a different subevent. However,
|
|
||||||
pretix always creates a new transaction whenever there is a change to a ticket that concerns the **price**, **tax rate**,
|
|
||||||
**product**, or **date** (in an event series).
|
|
||||||
|
|
||||||
The :ref:`rest-orders` themselves are not a good representation of the "Debit" side of the table for accounting
|
|
||||||
purposes since they are not immutable:
|
|
||||||
They will only tell you the current state of the order, not what it was a week ago.
|
|
||||||
|
|
||||||
The "Credit" column is represented by the :ref:`order-payment-resource` and :ref:`order-refund-resource`.
|
|
||||||
|
|
||||||
|
|
||||||
Resource description
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
.. rst-class:: rest-resource-table
|
|
||||||
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
Field Type Description
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
id integer Internal ID of the transaction
|
|
||||||
order string Order code the transaction was created from
|
|
||||||
event string Event slug, only present on organizer-level API calls
|
|
||||||
created datetime The creation time of the transaction in the database
|
|
||||||
datetime datetime The time at which the transaction is financially relevant.
|
|
||||||
This is usually the same as created, but may vary for
|
|
||||||
retroactively created transactions after software bugs or
|
|
||||||
for data that preceeds this data model.
|
|
||||||
positionid integer Number of the position within the order this refers to,
|
|
||||||
is ``null`` for transactions that refer to a fee
|
|
||||||
count integer Number of items purchased, is negative for cancellations
|
|
||||||
item integer The internal ID of the item purchased (or ``null`` for fees)
|
|
||||||
variation integer The internal ID of the variation purchased (or ``null``)
|
|
||||||
subevent integer The internal ID of the event series date (or ``null``)
|
|
||||||
price money (string) Gross price of the transaction
|
|
||||||
tax_rate decimal (string) Tax rate applied in transaction
|
|
||||||
tax_rule integer The internal ID of the tax rule used (or ``null``)
|
|
||||||
tax_code string The selected tax code (or ``null``)
|
|
||||||
tax_value money (string) The computed tax value
|
|
||||||
fee_type string The type of fee (or ``null`` for products)
|
|
||||||
internal_type string Additional type classification of the fee (or ``null`` for products)
|
|
||||||
===================================== ========================== =======================================================
|
|
||||||
|
|
||||||
.. versionchanged:: 2025.7.0
|
|
||||||
|
|
||||||
This resource was added to the API.
|
|
||||||
|
|
||||||
|
|
||||||
Endpoints
|
|
||||||
---------
|
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/transactions/
|
|
||||||
|
|
||||||
Returns a list of all transactions of an event.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/events/sampleconf/transactions/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"count": 1,
|
|
||||||
"next": null,
|
|
||||||
"previous": null,
|
|
||||||
"results": [
|
|
||||||
{
|
|
||||||
"id": 123,
|
|
||||||
"order": "FOO",
|
|
||||||
"count": 1,
|
|
||||||
"created": "2017-12-01T10:00:00Z",
|
|
||||||
"datetime": "2017-12-01T10:00:00Z",
|
|
||||||
"item": null,
|
|
||||||
"variation": null,
|
|
||||||
"positionid": 1,
|
|
||||||
"price": "23.00",
|
|
||||||
"subevent": null,
|
|
||||||
"tax_code": "E",
|
|
||||||
"tax_rate": "0.00",
|
|
||||||
"tax_rule": 23,
|
|
||||||
"tax_value": "0.00",
|
|
||||||
"fee_type": null,
|
|
||||||
"internal_type": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
|
||||||
:query string order: Only return transactions matching the given order code.
|
|
||||||
:query datetime_since: Only return transactions with a datetime at or after the given time.
|
|
||||||
:query datetime_before: Only return transactions with a datetime before the given time.
|
|
||||||
:query created_since: Only return transactions with a creation time at or after the given time.
|
|
||||||
:query created_before: Only return transactions with a creation time before the given time.
|
|
||||||
:query item: Only return transactions that match the given item ID.
|
|
||||||
:query item__in: Only return transactions that match one of the given item IDs (separated with a comma).
|
|
||||||
:query variation: Only return transactions that match the given variation ID.
|
|
||||||
:query variation__in: Only return transactions that match one of the given variation IDs (separated with a comma).
|
|
||||||
:query subevent: Only return transactions that match the given subevent ID.
|
|
||||||
:query subevent__in: Only return transactions that match one of the given subevent IDs (separated with a comma).
|
|
||||||
:query tax_rule: Only return transactions that match the given tax rule ID.
|
|
||||||
:query tax_rule__in: Only return transactions that match one of the given tax rule IDs (separated with a comma).
|
|
||||||
:query tax_code: Only return transactions that match the given tax code.
|
|
||||||
:query tax_code__in: Only return transactions that match one of the given tax codes (separated with a comma).
|
|
||||||
:query tax_rate: Only return transactions that match the given tax rate.
|
|
||||||
:query tax_rate__in: Only return transactions that match one of the given tax rates (separated with a comma).
|
|
||||||
:query fee_type: Only return transactions that match the given fee type.
|
|
||||||
:query fee_type__in: Only return transactions that match one of the given fee types (separated with a comma).
|
|
||||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``created``, and ``id``.
|
|
||||||
:param organizer: The ``slug`` field of a valid organizer
|
|
||||||
:param event: The ``slug`` field of a valid event
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer or event does not exist **or** you have no permission to view it.
|
|
||||||
|
|
||||||
.. http:get:: /api/v1/organizers/(organizer)/transactions/
|
|
||||||
|
|
||||||
Returns a list of all transactions of an organizer that you have access to.
|
|
||||||
|
|
||||||
**Example request**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
GET /api/v1/organizers/bigevents/transactions/ HTTP/1.1
|
|
||||||
Host: pretix.eu
|
|
||||||
Accept: application/json, text/javascript
|
|
||||||
|
|
||||||
**Example response**:
|
|
||||||
|
|
||||||
.. sourcecode:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Vary: Accept
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"count": 1,
|
|
||||||
"next": null,
|
|
||||||
"previous": null,
|
|
||||||
"results": [
|
|
||||||
{
|
|
||||||
"id": 123,
|
|
||||||
"event": "sampleconf",
|
|
||||||
"order": "FOO",
|
|
||||||
"count": 1,
|
|
||||||
"created": "2017-12-01T10:00:00Z",
|
|
||||||
"datetime": "2017-12-01T10:00:00Z",
|
|
||||||
"item": null,
|
|
||||||
"variation": null,
|
|
||||||
"positionid": 1,
|
|
||||||
"price": "23.00",
|
|
||||||
"subevent": null,
|
|
||||||
"tax_code": "E",
|
|
||||||
"tax_rate": "0.00",
|
|
||||||
"tax_rule": 23,
|
|
||||||
"tax_value": "0.00",
|
|
||||||
"fee_type": null,
|
|
||||||
"internal_type": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
|
||||||
:query string event: Only return transactions matching the given event slug.
|
|
||||||
:query string order: Only return transactions matching the given order code.
|
|
||||||
:query datetime_since: Only return transactions with a datetime at or after the given time.
|
|
||||||
:query datetime_before: Only return transactions with a datetime before the given time.
|
|
||||||
:query created_since: Only return transactions with a creation time at or after the given time.
|
|
||||||
:query created_before: Only return transactions with a creation time before the given time.
|
|
||||||
:query item: Only return transactions that match the given item ID.
|
|
||||||
:query item__in: Only return transactions that match one of the given item IDs (separated with a comma).
|
|
||||||
:query variation: Only return transactions that match the given variation ID.
|
|
||||||
:query variation__in: Only return transactions that match one of the given variation IDs (separated with a comma).
|
|
||||||
:query subevent: Only return transactions that match the given subevent ID.
|
|
||||||
:query subevent__in: Only return transactions that match one of the given subevent IDs (separated with a comma).
|
|
||||||
:query tax_rule: Only return transactions that match the given tax rule ID.
|
|
||||||
:query tax_rule__in: Only return transactions that match one of the given tax rule IDs (separated with a comma).
|
|
||||||
:query tax_code: Only return transactions that match the given tax code.
|
|
||||||
:query tax_code__in: Only return transactions that match one of the given tax codes (separated with a comma).
|
|
||||||
:query tax_rate: Only return transactions that match the given tax rate.
|
|
||||||
:query tax_rate__in: Only return transactions that match one of the given tax rates (separated with a comma).
|
|
||||||
:query fee_type: Only return transactions that match the given fee type.
|
|
||||||
:query fee_type__in: Only return transactions that match one of the given fee types (separated with a comma).
|
|
||||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``created``, and ``id``.
|
|
||||||
:param organizer: The ``slug`` field of a valid organizer
|
|
||||||
:statuscode 200: no error
|
|
||||||
:statuscode 401: Authentication failure
|
|
||||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
|
||||||
@@ -14,7 +14,6 @@ The voucher resource contains the following public fields:
|
|||||||
Field Type Description
|
Field Type Description
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
id integer Internal ID of the voucher
|
id integer Internal ID of the voucher
|
||||||
created datetime The creation date of the voucher. For vouchers created before pretix 2025.7.0, this is guessed retroactively and might not be accurate.
|
|
||||||
code string The voucher code that is required to redeem the voucher
|
code string The voucher code that is required to redeem the voucher
|
||||||
max_usages integer The maximum number of times this voucher can be
|
max_usages integer The maximum number of times this voucher can be
|
||||||
redeemed (default: 1).
|
redeemed (default: 1).
|
||||||
@@ -50,14 +49,8 @@ subevent integer ID of the date
|
|||||||
show_hidden_items boolean Only if set to ``true``, this voucher allows to buy products with the property ``hide_without_voucher``. Defaults to ``true``.
|
show_hidden_items boolean Only if set to ``true``, this voucher allows to buy products with the property ``hide_without_voucher``. Defaults to ``true``.
|
||||||
all_addons_included boolean If set to ``true``, all add-on products for the product purchased with this voucher are included in the base price.
|
all_addons_included boolean If set to ``true``, all add-on products for the product purchased with this voucher are included in the base price.
|
||||||
all_bundles_included boolean If set to ``true``, all bundled products for the product purchased with this voucher are added without their designated price.
|
all_bundles_included boolean If set to ``true``, all bundled products for the product purchased with this voucher are added without their designated price.
|
||||||
budget money (string) The budget a voucher is allowed to consume before being used up (or ``null``)
|
|
||||||
budget_used money (string) The amount of budget the voucher has already used up.
|
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
.. versionchanged:: 2025.7
|
|
||||||
|
|
||||||
The attributes ``created``, ``budget``, and ``budget_used`` have been added.
|
|
||||||
|
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
@@ -89,7 +82,6 @@ Endpoints
|
|||||||
"results": [
|
"results": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"created": "2020-09-18T14:17:40.971519Z",
|
|
||||||
"code": "43K6LKM37FBVR2YG",
|
"code": "43K6LKM37FBVR2YG",
|
||||||
"max_usages": 1,
|
"max_usages": 1,
|
||||||
"redeemed": 0,
|
"redeemed": 0,
|
||||||
@@ -107,9 +99,7 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"show_hidden_items": false,
|
"show_hidden_items": false,
|
||||||
"all_addons_included": false,
|
"all_addons_included": false,
|
||||||
"all_bundles_included": false,
|
"all_bundles_included": false
|
||||||
"budget": None,
|
|
||||||
"budget_used": "0.00"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -162,7 +152,6 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"created": "2020-09-18T14:17:40.971519Z",
|
|
||||||
"code": "43K6LKM37FBVR2YG",
|
"code": "43K6LKM37FBVR2YG",
|
||||||
"max_usages": 1,
|
"max_usages": 1,
|
||||||
"redeemed": 0,
|
"redeemed": 0,
|
||||||
@@ -180,9 +169,7 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"show_hidden_items": false,
|
"show_hidden_items": false,
|
||||||
"all_addons_included": false,
|
"all_addons_included": false,
|
||||||
"all_bundles_included": false,
|
"all_bundles_included": false
|
||||||
"budget": None,
|
|
||||||
"budget_used": "0.00"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -235,7 +222,6 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"created": "2020-09-18T14:17:40.971519Z",
|
|
||||||
"code": "43K6LKM37FBVR2YG",
|
"code": "43K6LKM37FBVR2YG",
|
||||||
"max_usages": 1,
|
"max_usages": 1,
|
||||||
"redeemed": 0,
|
"redeemed": 0,
|
||||||
@@ -253,9 +239,7 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"show_hidden_items": false,
|
"show_hidden_items": false,
|
||||||
"all_addons_included": false,
|
"all_addons_included": false,
|
||||||
"all_bundles_included": false,
|
"all_bundles_included": false
|
||||||
"budget": None,
|
|
||||||
"budget_used": "0.00"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to create a voucher for
|
:param organizer: The ``slug`` field of the organizer to create a voucher for
|
||||||
@@ -329,7 +313,6 @@ Endpoints
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"created": "2020-09-18T14:17:40.971519Z",
|
|
||||||
"code": "43K6LKM37FBVR2YG",
|
"code": "43K6LKM37FBVR2YG",
|
||||||
…
|
…
|
||||||
}, …
|
}, …
|
||||||
@@ -376,7 +359,6 @@ Endpoints
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"created": "2020-09-18T14:17:40.971519Z",
|
|
||||||
"code": "43K6LKM37FBVR2YG",
|
"code": "43K6LKM37FBVR2YG",
|
||||||
"max_usages": 1,
|
"max_usages": 1,
|
||||||
"redeemed": 0,
|
"redeemed": 0,
|
||||||
@@ -394,9 +376,7 @@ Endpoints
|
|||||||
"subevent": null,
|
"subevent": null,
|
||||||
"show_hidden_items": false,
|
"show_hidden_items": false,
|
||||||
"all_addons_included": false,
|
"all_addons_included": false,
|
||||||
"all_bundles_included": false,
|
"all_bundles_included": false
|
||||||
"budget": None,
|
|
||||||
"budget_used": "0.00"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
|||||||
@@ -60,12 +60,6 @@ The following values for ``action_types`` are valid with pretix core:
|
|||||||
* ``pretix.event.added``
|
* ``pretix.event.added``
|
||||||
* ``pretix.event.changed``
|
* ``pretix.event.changed``
|
||||||
* ``pretix.event.deleted``
|
* ``pretix.event.deleted``
|
||||||
* ``pretix.giftcards.created``
|
|
||||||
* ``pretix.giftcards.modified``
|
|
||||||
* ``pretix.giftcards.transaction.*``
|
|
||||||
* ``pretix.voucher.added``
|
|
||||||
* ``pretix.voucher.changed``
|
|
||||||
* ``pretix.voucher.deleted``
|
|
||||||
* ``pretix.subevent.added``
|
* ``pretix.subevent.added``
|
||||||
* ``pretix.subevent.changed``
|
* ``pretix.subevent.changed``
|
||||||
* ``pretix.subevent.deleted``
|
* ``pretix.subevent.deleted``
|
||||||
|
|||||||
@@ -178,124 +178,3 @@ Flowchart
|
|||||||
---------
|
---------
|
||||||
|
|
||||||
.. image:: /images/cart_pricing.png
|
.. image:: /images/cart_pricing.png
|
||||||
|
|
||||||
|
|
||||||
.. _`algorithms-rounding`:
|
|
||||||
|
|
||||||
Rounding of taxes
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
pretix internally always stores taxes on a per-line level, like this:
|
|
||||||
|
|
||||||
========== ========== =========== ======= =============
|
|
||||||
Product Tax rate Net price Tax Gross price
|
|
||||||
========== ========== =========== ======= =============
|
|
||||||
Ticket A 19 % 84.03 15.97 100.00
|
|
||||||
Ticket B 19 % 84.03 15.97 100.00
|
|
||||||
Ticket C 19 % 84.03 15.97 100.00
|
|
||||||
Ticket D 19 % 84.03 15.97 100.00
|
|
||||||
Ticket E 19 % 84.03 15.97 100.00
|
|
||||||
Sum 420.15 79.85 500.00
|
|
||||||
========== ========== =========== ======= =============
|
|
||||||
|
|
||||||
Whether the net price is computed from the gross price or vice versa is configured on the tax rule and may differ for every line.
|
|
||||||
|
|
||||||
The line-based computation has a few significant advantages:
|
|
||||||
|
|
||||||
- We can report both net and gross prices for every individual ticket.
|
|
||||||
|
|
||||||
- We can report both net and gross prices for every filter imaginable, such as the gross sum of all sales of Ticket A
|
|
||||||
or the net sum of all sales for a specific date in an event series. All numbers will be exact.
|
|
||||||
|
|
||||||
- When splitting the order into two, both net price and gross price are split without any changes in rounding.
|
|
||||||
|
|
||||||
The main disadvantage is that the tax looks "wrong" when computed from the sum. Taking the sum of net prices (420.15)
|
|
||||||
and multiplying it with the tax rate (19%) yields a tax amount of 79.83 (instead of 79.85) and a gross sum of 499.98
|
|
||||||
(instead of 500.00). This becomes a problem when juristictions, data formats, or external systems expect this calculation
|
|
||||||
to work on the level of the entire order. A prominent example is the EN 16931 standard for e-invoicing that
|
|
||||||
does not allow the computation as created by pretix.
|
|
||||||
|
|
||||||
However, calculating the tax rate from the net total has significant disadvantages:
|
|
||||||
|
|
||||||
- It is impossible to guarantee a stable gross price this way, i.e. if you advertise a price of €100 per ticket to
|
|
||||||
consumers, they will be confused when they only need to pay €499.98 for 5 tickets.
|
|
||||||
|
|
||||||
- Some prices are impossible, e.g. you cannot sell a ticket for a gross price of €99.99 at a 19% tax rate, since there
|
|
||||||
is no two-decimal net price that would be computed to a gross price of €99.99.
|
|
||||||
|
|
||||||
- When splitting an order into two, the combined of the new orders is not guaranteed to be the same as the total of the
|
|
||||||
original order. Therefore, additional payments or refunds of very small amounts might be necessary.
|
|
||||||
|
|
||||||
To allow organizers to make their own choices on this matter, pretix provides the following options:
|
|
||||||
|
|
||||||
Compute taxes for every line individually
|
|
||||||
"""""""""""""""""""""""""""""""""""""""""
|
|
||||||
|
|
||||||
Algorithm identifier: ``line``
|
|
||||||
|
|
||||||
This is our original algorithm where the tax value is rounded for every line individually.
|
|
||||||
|
|
||||||
**This is our current default algorithm and we recommend it whenever you do not have different requirements** (see below).
|
|
||||||
For the example above:
|
|
||||||
|
|
||||||
========== ========== =========== ======= =============
|
|
||||||
Product Tax rate Net price Tax Gross price
|
|
||||||
========== ========== =========== ======= =============
|
|
||||||
Ticket A 19 % 84.03 15.97 100.00
|
|
||||||
Ticket B 19 % 84.03 15.97 100.00
|
|
||||||
Ticket C 19 % 84.03 15.97 100.00
|
|
||||||
Ticket D 19 % 84.03 15.97 100.00
|
|
||||||
Ticket E 19 % 84.03 15.97 100.00
|
|
||||||
Sum 420.15 79.85 500.00
|
|
||||||
========== ========== =========== ======= =============
|
|
||||||
|
|
||||||
|
|
||||||
Compute taxes based on net total
|
|
||||||
""""""""""""""""""""""""""""""""
|
|
||||||
|
|
||||||
Algorithm identifier: ``sum_by_net``
|
|
||||||
|
|
||||||
In this algorithm, the tax value and gross total are computed from the sum of the net prices. To accomplish this within
|
|
||||||
our data model, the gross price and tax of some of the tickets will be changed by the minimum currency unit (e.g. €0.01).
|
|
||||||
The net price of the tickets always stay the same.
|
|
||||||
|
|
||||||
**This is the algorithm intended by EN 16931 invoices and our recommendation to use for e-invoicing when (primarily) business customers are involved.**
|
|
||||||
|
|
||||||
The main downside is that it might be confusing when selling to consumers, since the amounts to be paid change in unexpected ways.
|
|
||||||
For the example above, the customer expects to pay 5 times 100.00, but they are are in fact charged 499.98:
|
|
||||||
|
|
||||||
========== ========== =========== ============================== ==============================
|
|
||||||
Product Tax rate Net price Tax Gross price
|
|
||||||
========== ========== =========== ============================== ==============================
|
|
||||||
Ticket A 19 % 84.03 15.96 (incl. -0.01 rounding) 99.99 (incl. -0.01 rounding)
|
|
||||||
Ticket B 19 % 84.03 15.96 (incl. -0.01 rounding) 99.99 (incl. -0.01 rounding)
|
|
||||||
Ticket C 19 % 84.03 15.97 100.00
|
|
||||||
Ticket D 19 % 84.03 15.97 100.00
|
|
||||||
Ticket E 19 % 84.03 15.97 100.00
|
|
||||||
Sum 420.15 78.83 499.98
|
|
||||||
========== ========== =========== ============================== ==============================
|
|
||||||
|
|
||||||
Compute taxes based on net total with stable gross prices
|
|
||||||
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
|
||||||
|
|
||||||
Algorithm identifier: ``sum_by_net_keep_gross``
|
|
||||||
|
|
||||||
In this algorithm, the tax value and gross total are computed from the sum of the net prices. However, the net prices
|
|
||||||
of some of the tickets will be changed automatically by the minimum currency unit (e.g. €0.01) such that the resulting
|
|
||||||
gross prices stay the same.
|
|
||||||
|
|
||||||
**This is less confusing to consumers and the end result is still compliant to EN 16931, so we recommend this for e-invoicing when (primarily) consumers are involved.**
|
|
||||||
|
|
||||||
The main downside is that it might be confusing when selling to business customers, since the prices of the identical tickets appear to be different.
|
|
||||||
Full computation for the example above:
|
|
||||||
|
|
||||||
========== ========== ============================= ============================== =============
|
|
||||||
Product Tax rate Net price Tax Gross price
|
|
||||||
========== ========== ============================= ============================== =============
|
|
||||||
Ticket A 19 % 84.04 (incl. 0.01 rounding) 15.96 (incl. -0.01 rounding) 100.00
|
|
||||||
Ticket B 19 % 84.04 (incl. 0.01 rounding) 15.96 (incl. -0.01 rounding) 100.00
|
|
||||||
Ticket C 19 % 84.03 15.97 100.00
|
|
||||||
Ticket D 19 % 84.03 15.97 100.00
|
|
||||||
Ticket E 19 % 84.03 15.97 100.00
|
|
||||||
Sum 420.17 79.83 500.00
|
|
||||||
========== ========== ============================= ============================== =============
|
|
||||||
|
|||||||
@@ -55,12 +55,12 @@ your views:
|
|||||||
)
|
)
|
||||||
|
|
||||||
class AdminView(EventPermissionRequiredMixin, View):
|
class AdminView(EventPermissionRequiredMixin, View):
|
||||||
permission = 'event.orders:read'
|
permission = 'can_view_orders'
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
@event_permission_required('event.orders:read')
|
@event_permission_required('can_view_orders')
|
||||||
def admin_view(request, organizer, event):
|
def admin_view(request, organizer, event):
|
||||||
...
|
...
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ event-related views, there is also a signal that allows you to add the view to t
|
|||||||
@receiver(nav_event, dispatch_uid='friends_tickets_nav')
|
@receiver(nav_event, dispatch_uid='friends_tickets_nav')
|
||||||
def navbar_info(sender, request, **kwargs):
|
def navbar_info(sender, request, **kwargs):
|
||||||
url = resolve(request.path_info)
|
url = resolve(request.path_info)
|
||||||
if not request.user.has_event_permission(request.organizer, request.event, 'event.vouchers:read'):
|
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_vouchers'):
|
||||||
return []
|
return []
|
||||||
return [{
|
return [{
|
||||||
'label': _('My plugin view'),
|
'label': _('My plugin view'),
|
||||||
@@ -118,7 +118,7 @@ for good integration. If you just want to display a form, you could do it like t
|
|||||||
|
|
||||||
class MySettingsView(EventSettingsViewMixin, EventSettingsFormView):
|
class MySettingsView(EventSettingsViewMixin, EventSettingsFormView):
|
||||||
model = Event
|
model = Event
|
||||||
permission = 'event.settings.general:write'
|
permission = 'can_change_settings'
|
||||||
form_class = MySettingsForm
|
form_class = MySettingsForm
|
||||||
template_name = 'my_plugin/settings.html'
|
template_name = 'my_plugin/settings.html'
|
||||||
|
|
||||||
@@ -204,13 +204,13 @@ In case of ``orga_router`` and ``event_router``, permission checking is done for
|
|||||||
in the control panel. However, you need to make sure on your own only to return the correct subset of data! ``request
|
in the control panel. However, you need to make sure on your own only to return the correct subset of data! ``request
|
||||||
.event`` and ``request.organizer`` are available as usual.
|
.event`` and ``request.organizer`` are available as usual.
|
||||||
|
|
||||||
To require a special permission like ``event.orders:read``, you do not need to inherit from a special ViewSet base
|
To require a special permission like ``can_view_orders``, you do not need to inherit from a special ViewSet base
|
||||||
class, you can just set the ``permission`` attribute on your viewset:
|
class, you can just set the ``permission`` attribute on your viewset:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
class MyViewSet(ModelViewSet):
|
class MyViewSet(ModelViewSet):
|
||||||
permission = 'event.orders:read'
|
permission = 'can_view_orders'
|
||||||
...
|
...
|
||||||
|
|
||||||
If you want to check the permission only for some methods of your viewset, you have to do it yourself. Note here that
|
If you want to check the permission only for some methods of your viewset, you have to do it yourself. Note here that
|
||||||
@@ -220,7 +220,7 @@ following:
|
|||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken) else request.user)
|
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken) else request.user)
|
||||||
if perm_holder.has_event_permission(request.event.organizer, request.event, 'event.orders:read'):
|
if perm_holder.has_event_permission(request.event.organizer, request.event, 'can_view_orders'):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,207 +0,0 @@
|
|||||||
.. highlight:: python
|
|
||||||
:linenothreshold: 5
|
|
||||||
|
|
||||||
Data sync providers
|
|
||||||
===================
|
|
||||||
|
|
||||||
.. warning:: This feature is considered **experimental**. It might change at any time without prior notice.
|
|
||||||
|
|
||||||
pretix provides connectivity to many external services through plugins. A common requirement
|
|
||||||
is unidirectionally sending (order, customer, ticket, ...) data into external systems.
|
|
||||||
The transfer is usually triggered by signals provided by pretix core (e.g. :data:`order_placed`),
|
|
||||||
but performed asynchronously.
|
|
||||||
|
|
||||||
Such plugins should use the :class:`OutboundSyncProvider` API to utilize the queueing, retry and mapping
|
|
||||||
mechanisms as well as the user interface for configuration and monitoring. Sync providers are registered
|
|
||||||
in the :py:attr:`pretix.base.datasync.datasync.datasync_providers` :ref:`registry <registries>`.
|
|
||||||
|
|
||||||
An :class:`OutboundSyncProvider` for subscribing event participants to a mailing list could start
|
|
||||||
like this, for example:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from pretix.base.datasync.datasync import (OutboundSyncProvider, datasync_providers)
|
|
||||||
|
|
||||||
@datasync_providers.register
|
|
||||||
class MyListSyncProvider(OutboundSyncProvider):
|
|
||||||
identifier = "my_list"
|
|
||||||
display_name = "My Mailing List Service"
|
|
||||||
# ...
|
|
||||||
|
|
||||||
|
|
||||||
The plugin must register listeners in `signals.py` for all signals that should to trigger a sync and
|
|
||||||
within it has to call :meth:`MyListSyncProvider.enqueue_order` to enqueue the order for synchronization:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
@receiver(order_placed, dispatch_uid="mylist_order_placed")
|
|
||||||
def on_order_placed(sender, order, **kwargs):
|
|
||||||
MyListSyncProvider.enqueue_order(order, "order_placed")
|
|
||||||
|
|
||||||
|
|
||||||
Property mappings
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
Most of these plugins need to translate data from some pretix objects (e.g. orders)
|
|
||||||
into an external system's data structures. Sometimes, there is only one reasonable way or the
|
|
||||||
plugin author makes an opinionated decision what information from which objects should be
|
|
||||||
transferred into which data structures in the external system.
|
|
||||||
|
|
||||||
Otherwise, you can use a :class:`PropertyMappingFormSet` to let the user set up a mapping from pretix model fields
|
|
||||||
to external data fields. You could store the mapping information either in the event settings, or in a separate
|
|
||||||
data model. Your implementation of :attr:`OutboundSyncProvider.mappings`
|
|
||||||
needs to provide a list of mappings, which can be e.g. static objects or model instances, as long as they
|
|
||||||
have at least the properties defined in
|
|
||||||
:class:`pretix.base.datasync.datasync.StaticMapping`.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# class MyListSyncProvider, contd.
|
|
||||||
def mappings(self):
|
|
||||||
return [
|
|
||||||
StaticMapping(
|
|
||||||
id=1, pretix_model='Order', external_object_type='Contact',
|
|
||||||
pretix_id_field='email', external_id_field='email',
|
|
||||||
property_mappings=self.event.settings.mylist_order_mapping,
|
|
||||||
))
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
Currently, we support `orders` and `order positions` as data sources, with the data fields defined in
|
|
||||||
:func:`pretix.base.datasync.sourcefields.get_data_fields`.
|
|
||||||
|
|
||||||
To perform the actual sync, implement :func:`sync_object_with_properties` and optionally
|
|
||||||
:func:`finalize_sync_order`. The former is called for each object to be created according to the ``mappings``.
|
|
||||||
For each order that was enqueued using :func:`enqueue_order`:
|
|
||||||
|
|
||||||
- each Mapping with ``pretix_model == "Order"`` results in one call to :func:`sync_object_with_properties`,
|
|
||||||
- each Mapping with ``pretix_model == "OrderPosition"`` results in one call to
|
|
||||||
:func:`sync_object_with_properties` per order position,
|
|
||||||
- :func:`finalize_sync_order` is called one time after all calls to :func:`sync_object_with_properties`.
|
|
||||||
|
|
||||||
|
|
||||||
Implementation examples
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
For example implementations, see the test cases in :mod:`tests.base.test_datasync`.
|
|
||||||
|
|
||||||
In :class:`SimpleOrderSync`, a basic data transfer of order data only is
|
|
||||||
shown. Therein, a ``sync_object_with_properties`` method is defined as follows:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from pretix.base.datasync.utils import assign_properties
|
|
||||||
|
|
||||||
# class MyListSyncProvider, contd.
|
|
||||||
def sync_object_with_properties(
|
|
||||||
self, external_id_field, id_value, properties: list, inputs: dict,
|
|
||||||
mapping, mapped_objects: dict, **kwargs,
|
|
||||||
):
|
|
||||||
# First, we query the external service if our object-to-sync already exists there.
|
|
||||||
# This is necessary to make sure our method is idempotent, i.e. handles already synced
|
|
||||||
# data gracefully.
|
|
||||||
pre_existing_object = self.fake_api_client.retrieve_object(
|
|
||||||
mapping.external_object_type,
|
|
||||||
external_id_field,
|
|
||||||
id_value
|
|
||||||
)
|
|
||||||
|
|
||||||
# We use the helper function ``assign_properties`` to update a pre-existing object.
|
|
||||||
update_values = assign_properties(
|
|
||||||
new_values=properties,
|
|
||||||
old_values=pre_existing_object or {},
|
|
||||||
is_new=pre_existing_object is None,
|
|
||||||
list_sep=";",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Then we can send our new data to the external service. The specifics of course depends
|
|
||||||
# on your API, e.g. you may need to use different endpoints for creating or updating an
|
|
||||||
# object, or pass the identifier separately instead of in the same dictionary as the
|
|
||||||
# other properties.
|
|
||||||
result = self.fake_api_client.create_or_update_object(mapping.external_object_type, {
|
|
||||||
**update_values,
|
|
||||||
external_id_field: id_value,
|
|
||||||
"_id": pre_existing_object and pre_existing_object.get("_id"),
|
|
||||||
})
|
|
||||||
|
|
||||||
# Finally, return a dictionary containing at least `object_type`, `external_id_field`,
|
|
||||||
# `id_value`, `external_link_href`, and `external_link_display_name` keys.
|
|
||||||
# Further keys may be provided for your internal use. This dictionary is provided
|
|
||||||
# in following calls in the ``mapped_objects`` dict, to allow creating associations
|
|
||||||
# to this object.
|
|
||||||
return {
|
|
||||||
"object_type": mapping.external_object_type,
|
|
||||||
"external_id_field": external_id_field,
|
|
||||||
"id_value": id_value,
|
|
||||||
"external_link_href": f"https://example.org/external-system/{mapping.external_object_type}/{id_value}/",
|
|
||||||
"external_link_display_name": f"Contact #{id_value} - Jane Doe",
|
|
||||||
"my_result": result,
|
|
||||||
}
|
|
||||||
|
|
||||||
.. note:: The result dictionaries of earlier invocations of :func:`sync_object_with_properties` are
|
|
||||||
only provided in subsequent calls of the same sync run, such that a mapping can
|
|
||||||
refer to e.g. the external id of an object created by a preceding mapping.
|
|
||||||
However, the result dictionaries are currently not provided across runs. This will
|
|
||||||
likely change in a future revision of this API, to allow easier integration of external
|
|
||||||
systems that do not allow retrieving/updating data by a pretix-provided key.
|
|
||||||
|
|
||||||
``mapped_objects`` is a dictionary of lists of dictionaries. The keys to the dictionary are
|
|
||||||
the mapping identifiers (``mapping.id``), the lists contain the result dictionaries returned
|
|
||||||
by :func:`sync_object_with_properties`.
|
|
||||||
|
|
||||||
|
|
||||||
In :class:`OrderAndTicketAssociationSync`, an example is given where orders, order positions,
|
|
||||||
and the association between them are transferred.
|
|
||||||
|
|
||||||
|
|
||||||
The OutboundSyncProvider base class
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
.. autoclass:: pretix.base.datasync.datasync.OutboundSyncProvider
|
|
||||||
:members:
|
|
||||||
|
|
||||||
|
|
||||||
Property mapping format
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
To allow the user to configure property mappings, you can use the PropertyMappingFormSet,
|
|
||||||
which will generate the required ``property_mappings`` value automatically. If you need
|
|
||||||
to specify the property mappings programmatically, you can refer to the description below
|
|
||||||
on their format.
|
|
||||||
|
|
||||||
.. autoclass:: pretix.control.forms.mapping.PropertyMappingFormSet
|
|
||||||
:members: to_property_mappings_json
|
|
||||||
|
|
||||||
A simple JSON-serialized ``property_mappings`` list for mapping some order information can look like this:
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"pretix_field": "email",
|
|
||||||
"external_field": "orderemail",
|
|
||||||
"value_map": "",
|
|
||||||
"overwrite": "overwrite",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pretix_field": "order_status",
|
|
||||||
"external_field": "status",
|
|
||||||
"value_map": "{\"n\": \"pending\", \"p\": \"paid\", \"e\": \"expired\", \"c\": \"canceled\", \"r\": \"refunded\"}",
|
|
||||||
"overwrite": "overwrite",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pretix_field": "order_total",
|
|
||||||
"external_field": "total",
|
|
||||||
"value_map": "",
|
|
||||||
"overwrite": "overwrite",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
Translating mappings on Event copy
|
|
||||||
----------------------------------
|
|
||||||
|
|
||||||
Property mappings can contain references to event-specific primary keys. Therefore, plugins must register to the
|
|
||||||
event_copy_data signal and call translate_property_mappings on all property mappings they store.
|
|
||||||
|
|
||||||
.. autofunction:: pretix.base.datasync.utils.translate_property_mappings
|
|
||||||
@@ -80,24 +80,8 @@ The exporter class
|
|||||||
|
|
||||||
.. autoattribute:: category
|
.. autoattribute:: category
|
||||||
|
|
||||||
.. autoattribute:: feature
|
|
||||||
|
|
||||||
.. autoattribute:: export_form_fields
|
.. autoattribute:: export_form_fields
|
||||||
|
|
||||||
.. autoattribute:: repeatable_read
|
|
||||||
|
|
||||||
.. automethod:: render
|
.. automethod:: render
|
||||||
|
|
||||||
This is an abstract method, you **must** override this!
|
This is an abstract method, you **must** override this!
|
||||||
|
|
||||||
.. automethod:: available_for_user
|
|
||||||
|
|
||||||
.. automethod:: get_required_event_permission
|
|
||||||
|
|
||||||
On organizer level, by default exporters are expected to handle on a *set of events* and the system will automatically
|
|
||||||
add a form field that allows the selection of events, limited to events the user has correct permissions for. If this
|
|
||||||
does not fit your organizer, because it is not related to events, you should **also** inherit from the following class:
|
|
||||||
|
|
||||||
.. class:: pretix.base.exporter.OrganizerLevelExportMixin
|
|
||||||
|
|
||||||
.. automethod:: get_required_organizer_permission
|
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ Core
|
|||||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
|
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
|
||||||
item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter,
|
item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter,
|
||||||
register_ticket_secret_generators, gift_card_transaction_display,
|
register_ticket_secret_generators, gift_card_transaction_display,
|
||||||
register_text_placeholders, register_mail_placeholders, device_info_updated,
|
register_text_placeholders, register_mail_placeholders, device_info_updated
|
||||||
register_event_permission_groups, register_organizer_permission_groups
|
|
||||||
|
|
||||||
Order events
|
Order events
|
||||||
""""""""""""
|
""""""""""""
|
||||||
@@ -24,21 +23,21 @@ There are multiple signals that will be sent out in the ordering cycle:
|
|||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:no-index:
|
:no-index:
|
||||||
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_expiry_changed, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, build_invoice_data, invoice_line_text
|
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_expiry_changed, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
|
||||||
|
|
||||||
Check-ins
|
Check-ins
|
||||||
"""""""""
|
"""""""""
|
||||||
|
|
||||||
.. automodule:: pretix.base.signals
|
.. automodule:: pretix.base.signals
|
||||||
:no-index:
|
:no-index:
|
||||||
:members: checkin_created, checkin_annulled
|
:members: checkin_created
|
||||||
|
|
||||||
|
|
||||||
Frontend
|
Frontend
|
||||||
--------
|
--------
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header, seatingframe_html_head, filter_subevents
|
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header, seatingframe_html_head
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
|
|||||||
@@ -13,12 +13,10 @@ Contents:
|
|||||||
email
|
email
|
||||||
placeholder
|
placeholder
|
||||||
invoice
|
invoice
|
||||||
invoicetransmission
|
|
||||||
shredder
|
shredder
|
||||||
import
|
import
|
||||||
customview
|
customview
|
||||||
cookieconsent
|
cookieconsent
|
||||||
auth
|
auth
|
||||||
datasync
|
|
||||||
general
|
general
|
||||||
quality
|
quality
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
.. highlight:: python
|
|
||||||
:linenothreshold: 5
|
|
||||||
|
|
||||||
Writing an invoice transmission plugin
|
|
||||||
======================================
|
|
||||||
|
|
||||||
An invoice transmission provider transports an invoice from the sender to the recipient.
|
|
||||||
There are pre-defined types of invoice transmission in pretix, currently ``"email"``, ``"peppol"``, and ``"it_sdi"``.
|
|
||||||
You can find more information about them at :ref:`rest-transmission-types`.
|
|
||||||
|
|
||||||
New transmission types can not be added by plugins but need to be added to pretix itself.
|
|
||||||
However, plugins can provide implementations for the actual transmission.
|
|
||||||
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
|
|
||||||
|
|
||||||
Output registration
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
New invoice transmission providers can be registered through the :ref:`registry <registries>` mechanism
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from pretix.base.invoicing.transmission import transmission_providers, TransmissionProvider
|
|
||||||
|
|
||||||
@transmission_providers.new()
|
|
||||||
class SdiTransmissionProvider(TransmissionProvider):
|
|
||||||
identifier = "fatturapa_providerabc"
|
|
||||||
type = "it_sdi"
|
|
||||||
verbose_name = _("FatturaPA through provider ABC")
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
The provider class
|
|
||||||
------------------
|
|
||||||
|
|
||||||
.. class:: pretix.base.invoicing.transmission.TransmissionProvider
|
|
||||||
|
|
||||||
.. autoattribute:: identifier
|
|
||||||
|
|
||||||
This is an abstract attribute, you **must** override this!
|
|
||||||
|
|
||||||
.. autoattribute:: type
|
|
||||||
|
|
||||||
This is an abstract attribute, you **must** override this!
|
|
||||||
|
|
||||||
.. autoattribute:: verbose_name
|
|
||||||
|
|
||||||
This is an abstract attribute, you **must** override this!
|
|
||||||
|
|
||||||
.. autoattribute:: priority
|
|
||||||
|
|
||||||
.. autoattribute:: testmode_supported
|
|
||||||
|
|
||||||
.. automethod:: is_ready
|
|
||||||
|
|
||||||
This is an abstract method, you **must** override this!
|
|
||||||
|
|
||||||
.. automethod:: is_available
|
|
||||||
|
|
||||||
This is an abstract method, you **must** override this!
|
|
||||||
|
|
||||||
.. automethod:: transmit
|
|
||||||
|
|
||||||
This is an abstract method, you **must** override this!
|
|
||||||
|
|
||||||
.. automethod:: settings_url
|
|
||||||
@@ -56,20 +56,6 @@ restricted boolean (optional) ``False`` by default, restricts a plugin
|
|||||||
for an event by system administrators / superusers.
|
for an event by system administrators / superusers.
|
||||||
experimental boolean (optional) ``False`` by default, marks a plugin as an experimental feature in the plugins list.
|
experimental boolean (optional) ``False`` by default, marks a plugin as an experimental feature in the plugins list.
|
||||||
compatibility string Specifier for compatible pretix versions.
|
compatibility string Specifier for compatible pretix versions.
|
||||||
level string System level the plugin can be activated at.
|
|
||||||
Set to ``pretix.base.plugins.PLUGIN_LEVEL_EVENT`` for plugins that can be activated
|
|
||||||
at event level and then be active for that event only.
|
|
||||||
Set to ``pretix.base.plugins.PLUGIN_LEVEL_ORGANIZER`` for plugins that can be
|
|
||||||
activated only for the organizer as a whole and are active for any event within
|
|
||||||
that organizer.
|
|
||||||
Set to ``pretix.base.plugins.PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID`` for plugins that
|
|
||||||
can be activated at organizer level but are considered active only within events
|
|
||||||
for which they have also been specifically activated.
|
|
||||||
More levels, e.g. user-level plugins, might be invented in the future.
|
|
||||||
settings_links list List of ``((menu name, submenu name, …), urlname, url_kwargs)`` tuples that point
|
|
||||||
to the plugin's settings.
|
|
||||||
navigation_links list List of ``((menu name, submenu name, …), urlname, url_kwargs)`` tuples that point
|
|
||||||
to the plugin's system pages.
|
|
||||||
================== ==================== ===========================================================
|
================== ==================== ===========================================================
|
||||||
|
|
||||||
A working example would be:
|
A working example would be:
|
||||||
@@ -77,9 +63,9 @@ A working example would be:
|
|||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pretix.base.plugins import PluginConfig, PLUGIN_LEVEL_EVENT
|
from pretix.base.plugins import PluginConfig
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise RuntimeError("Please use pretix 2025.7 or above to run this plugin!")
|
raise RuntimeError("Please use pretix 2.7 or above to run this plugin!")
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
@@ -93,7 +79,6 @@ A working example would be:
|
|||||||
version = '1.0.0'
|
version = '1.0.0'
|
||||||
category = 'PAYMENT'
|
category = 'PAYMENT'
|
||||||
picture = 'pretix_paypal/paypal_logo.svg'
|
picture = 'pretix_paypal/paypal_logo.svg'
|
||||||
level = PLUGIN_LEVEL_EVENT
|
|
||||||
visible = True
|
visible = True
|
||||||
featured = False
|
featured = False
|
||||||
restricted = False
|
restricted = False
|
||||||
@@ -157,14 +142,14 @@ method to make your receivers available:
|
|||||||
from . import signals # NOQA
|
from . import signals # NOQA
|
||||||
|
|
||||||
You can optionally specify code that is executed when your plugin is activated for an event
|
You can optionally specify code that is executed when your plugin is activated for an event
|
||||||
or organizer in the ``installed`` method:
|
in the ``installed`` method:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
class PaypalApp(AppConfig):
|
class PaypalApp(AppConfig):
|
||||||
…
|
…
|
||||||
|
|
||||||
def installed(self, event_or_organizer):
|
def installed(self, event):
|
||||||
pass # Your code here
|
pass # Your code here
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ A simple implementation could look like this:
|
|||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
class MyNotificationType(NotificationType):
|
class MyNotificationType(NotificationType):
|
||||||
required_permission = "event.orders:read"
|
required_permission = "can_view_orders"
|
||||||
action_type = "pretix.event.order.paid"
|
action_type = "pretix.event.order.paid"
|
||||||
verbose_name = _("Order has been paid")
|
verbose_name = _("Order has been paid")
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ Permissions
|
|||||||
===========
|
===========
|
||||||
|
|
||||||
pretix uses a fine-grained permission system to control who is allowed to control what parts of the system.
|
pretix uses a fine-grained permission system to control who is allowed to control what parts of the system.
|
||||||
The central concept here is the concept of *Teams*. You can read more on `configuring teams and permissions`_
|
The central concept here is the concept of *Teams*. You can read more on `configuring teams and permissions <user-teams>`_
|
||||||
and the :class:`pretix.base.models.Team` model in the respective parts of the documentation. The basic digest is:
|
and the :class:`pretix.base.models.Team` model in the respective parts of the documentation. The basic digest is:
|
||||||
An organizer account can have any number of teams, and any number of users can be part of a team. A team can be
|
An organizer account can have any number of teams, and any number of users can be part of a team. A team can be
|
||||||
assigned a set of permissions and connected to some or all of the events of the organizer.
|
assigned a set of permissions and connected to some or all of the events of the organizer.
|
||||||
@@ -25,8 +25,8 @@ permission level to access a view:
|
|||||||
|
|
||||||
|
|
||||||
class MyOrgaView(OrganizerPermissionRequiredMixin, View):
|
class MyOrgaView(OrganizerPermissionRequiredMixin, View):
|
||||||
permission = 'organizer.settings.general:write'
|
permission = 'can_change_organizer_settings'
|
||||||
# Only users with the permission ``organizer.settings.general:write`` on
|
# Only users with the permission ``can_change_organizer_settings`` on
|
||||||
# this organizer can access this
|
# this organizer can access this
|
||||||
|
|
||||||
|
|
||||||
@@ -35,9 +35,9 @@ permission level to access a view:
|
|||||||
# Only users with *any* permission on this organizer can access this
|
# Only users with *any* permission on this organizer can access this
|
||||||
|
|
||||||
|
|
||||||
@organizer_permission_required('organizer.settings.general:write')
|
@organizer_permission_required('can_change_organizer_settings')
|
||||||
def my_orga_view(request, organizer, **kwargs):
|
def my_orga_view(request, organizer, **kwargs):
|
||||||
# Only users with the permission ``organizer.settings.general:write`` on
|
# Only users with the permission ``can_change_organizer_settings`` on
|
||||||
# this organizer can access this
|
# this organizer can access this
|
||||||
|
|
||||||
|
|
||||||
@@ -56,8 +56,8 @@ Of course, the same is available on event level:
|
|||||||
|
|
||||||
|
|
||||||
class MyEventView(EventPermissionRequiredMixin, View):
|
class MyEventView(EventPermissionRequiredMixin, View):
|
||||||
permission = 'event.settings.general:write'
|
permission = 'can_change_event_settings'
|
||||||
# Only users with the permission ``event.settings.general:write`` on
|
# Only users with the permission ``can_change_event_settings`` on
|
||||||
# this event can access this
|
# this event can access this
|
||||||
|
|
||||||
|
|
||||||
@@ -65,16 +65,13 @@ Of course, the same is available on event level:
|
|||||||
permission = None
|
permission = None
|
||||||
# Only users with *any* permission on this event can access this
|
# Only users with *any* permission on this event can access this
|
||||||
|
|
||||||
class MyThirdEventView(EventPermissionRequiredMixin, View):
|
|
||||||
permission = AnyPermissionOf('event.settings.payment:write', 'event.settings.general:write')
|
|
||||||
# Only users with at least one of the specified permissions on this event
|
|
||||||
# can access this
|
|
||||||
|
|
||||||
@event_permission_required('event.settings.general:write')
|
@event_permission_required('can_change_event_settings')
|
||||||
def my_event_view(request, organizer, **kwargs):
|
def my_event_view(request, organizer, **kwargs):
|
||||||
# Only users with the permission ``event.settings.general:write`` on
|
# Only users with the permission ``can_change_event_settings`` on
|
||||||
# this event can access this
|
# this event can access this
|
||||||
|
|
||||||
|
|
||||||
@event_permission_required()
|
@event_permission_required()
|
||||||
def my_other_event_view(request, organizer, **kwargs):
|
def my_other_event_view(request, organizer, **kwargs):
|
||||||
# Only users with *any* permission on this event can access this
|
# Only users with *any* permission on this event can access this
|
||||||
@@ -124,7 +121,7 @@ When creating your own ``viewset`` using Django REST framework, you just need to
|
|||||||
and pretix will check it automatically for you::
|
and pretix will check it automatically for you::
|
||||||
|
|
||||||
class MyModelViewSet(viewsets.ReadOnlyModelViewSet):
|
class MyModelViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
permission = 'event.orders:read'
|
permission = 'can_view_orders'
|
||||||
|
|
||||||
Checking permission in code
|
Checking permission in code
|
||||||
---------------------------
|
---------------------------
|
||||||
@@ -139,12 +136,12 @@ Return all users that are in any team that is connected to this event::
|
|||||||
|
|
||||||
Return all users that are in a team with a specific permission for this event::
|
Return all users that are in a team with a specific permission for this event::
|
||||||
|
|
||||||
>>> event.get_users_with_permission('event.orders:read')
|
>>> event.get_users_with_permission('can_change_event_settings')
|
||||||
<QuerySet: …>
|
<QuerySet: …>
|
||||||
|
|
||||||
Determine if a user has a certain permission for a specific event::
|
Determine if a user has a certain permission for a specific event::
|
||||||
|
|
||||||
>>> user.has_event_permission(organizer, event, 'event.orders:read', request=request)
|
>>> user.has_event_permission(organizer, event, 'can_change_event_settings', request=request)
|
||||||
True
|
True
|
||||||
|
|
||||||
Determine if a user has any permission for a specific event::
|
Determine if a user has any permission for a specific event::
|
||||||
@@ -156,27 +153,27 @@ In the two previous commands, the ``request`` argument is optional, but required
|
|||||||
|
|
||||||
The same method exists for organizer-level permissions::
|
The same method exists for organizer-level permissions::
|
||||||
|
|
||||||
>>> user.has_organizer_permission(organizer, 'event.orders:read', request=request)
|
>>> user.has_organizer_permission(organizer, 'can_change_event_settings', request=request)
|
||||||
True
|
True
|
||||||
|
|
||||||
Sometimes, it might be more useful to get the set of permissions at once::
|
Sometimes, it might be more useful to get the set of permissions at once::
|
||||||
|
|
||||||
>>> user.get_event_permission_set(organizer, event)
|
>>> user.get_event_permission_set(organizer, event)
|
||||||
{'event.settings.general:write', 'event.orders:read', 'event.orders:write'}
|
{'can_change_event_settings', 'can_view_orders', 'can_change_orders'}
|
||||||
|
|
||||||
>>> user.get_organizer_permission_set(organizer, event)
|
>>> user.get_organizer_permission_set(organizer, event)
|
||||||
{'organizer.settings.general:write', 'organizer.events:create'}
|
{'can_change_organizer_settings', 'can_create_events'}
|
||||||
|
|
||||||
Within a view on the ``/control`` subpath, the results of these two methods are already available in the
|
Within a view on the ``/control`` subpath, the results of these two methods are already available in the
|
||||||
``request.eventpermset`` and ``request.orgapermset`` properties. This makes it convenient to query them in templates::
|
``request.eventpermset`` and ``request.orgapermset`` properties. This makes it convenient to query them in templates::
|
||||||
|
|
||||||
{% if "event.orders:write" in request.eventpermset %}
|
{% if "can_change_orders" in request.eventpermset %}
|
||||||
…
|
…
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
You can also do the reverse to get any events a user has access to::
|
You can also do the reverse to get any events a user has access to::
|
||||||
|
|
||||||
>>> user.get_events_with_permission('event.settings.general:write', request=request)
|
>>> user.get_events_with_permission('can_change_event_settings', request=request)
|
||||||
<QuerySet: …>
|
<QuerySet: …>
|
||||||
|
|
||||||
>>> user.get_events_with_any_permission(request=request)
|
>>> user.get_events_with_any_permission(request=request)
|
||||||
@@ -198,53 +195,3 @@ staff mode is active. You can check if a user is in staff mode using their sessi
|
|||||||
Staff mode has a hard time limit and during staff mode, a middleware will log all requests made by that user. Later,
|
Staff mode has a hard time limit and during staff mode, a middleware will log all requests made by that user. Later,
|
||||||
the user is able to also save a message to comment on what they did in their administrative session. This feature is
|
the user is able to also save a message to comment on what they did in their administrative session. This feature is
|
||||||
intended to help compliance with data protection rules as imposed e.g. by GDPR.
|
intended to help compliance with data protection rules as imposed e.g. by GDPR.
|
||||||
|
|
||||||
Adding permissions
|
|
||||||
------------------
|
|
||||||
|
|
||||||
Plugins can add permissions through the ``register_event_permission_groups`` and ``register_organizer_permission_groups``.
|
|
||||||
We recommend to use this only for very significant permissions, as the system will become less usable with too many
|
|
||||||
permission levels, also because the team page will show all permission options, even those of disabled plugins.
|
|
||||||
|
|
||||||
To register your permissions, you need to register a **permission group** (often representing an area of functionality
|
|
||||||
or a key model). Below that group, there are **actions**, which represent the actual permissions. Permissions will be
|
|
||||||
generated as ``<group_name>:<action>``. Then, you need to define **options** which are the valid combinations of the
|
|
||||||
actions that should be possible to select for a team. This two-step mechanism exists to provide a better user experience
|
|
||||||
and avoid useless combinations like "write but not read".
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
@receiver(register_event_permission_groups)
|
|
||||||
def register_plugin_event_permissions(sender, **kwargs):
|
|
||||||
return [
|
|
||||||
PermissionGroup(
|
|
||||||
name="pretix_myplugin.resource",
|
|
||||||
label=_("Resources"),
|
|
||||||
actions=["read", "write"],
|
|
||||||
options=[
|
|
||||||
PermissionOption(actions=tuple(), label=_("No access")),
|
|
||||||
PermissionOption(actions=("read",), label=_("View")),
|
|
||||||
PermissionOption(actions=("read", "write"), label=_("View and change")),
|
|
||||||
],
|
|
||||||
help_text=_("Some help text")
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(register_organizer_permission_groups)
|
|
||||||
def register_plugin_organizer_permissions(sender, **kwargs):
|
|
||||||
return [
|
|
||||||
PermissionGroup(
|
|
||||||
name="pretix_myplugin.resource",
|
|
||||||
label=_("Resources"),
|
|
||||||
actions=["read", "write"],
|
|
||||||
options=[
|
|
||||||
PermissionOption(actions=tuple(), label=_("No access")),
|
|
||||||
PermissionOption(actions=("read",), label=_("View")),
|
|
||||||
PermissionOption(actions=("read", "write"), label=_("View and change")),
|
|
||||||
],
|
|
||||||
help_text=_("Some help text")
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
.. _configuring teams and permissions: https://docs.pretix.eu/guides/teams/
|
|
||||||
@@ -5,7 +5,7 @@ Development setup
|
|||||||
|
|
||||||
This tutorial helps you to get started hacking with pretix on your own computer. You need this to
|
This tutorial helps you to get started hacking with pretix on your own computer. You need this to
|
||||||
be able to contribute to pretix, but it might also be helpful if you want to write your own plugins.
|
be able to contribute to pretix, but it might also be helpful if you want to write your own plugins.
|
||||||
If you want to install pretix on a server for actual usage, go to the `administrator documentation`_ instead.
|
If you want to install pretix on a server for actual usage, go to the [administrator documentation](https://docs.pretix.eu/self-hosting/) instead.
|
||||||
|
|
||||||
Obtain a copy of the source code
|
Obtain a copy of the source code
|
||||||
--------------------------------
|
--------------------------------
|
||||||
@@ -221,4 +221,3 @@ your virtual environment.::
|
|||||||
|
|
||||||
.. _Django's documentation: https://docs.djangoproject.com/en/1.11/ref/django-admin/#runserver
|
.. _Django's documentation: https://docs.djangoproject.com/en/1.11/ref/django-admin/#runserver
|
||||||
.. _pretixdroid: https://github.com/pretix/pretixdroid
|
.. _pretixdroid: https://github.com/pretix/pretixdroid
|
||||||
.. _administrator documentation: https://docs.pretix.eu/self-hosting/
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 54 KiB |
@@ -23,7 +23,6 @@ partition "For every cart position" {
|
|||||||
--> "Store as line_price (gross), tax_rate"
|
--> "Store as line_price (gross), tax_rate"
|
||||||
}
|
}
|
||||||
--> "Apply discount engine"
|
--> "Apply discount engine"
|
||||||
--> "Apply tax rounding"
|
|
||||||
--> "Store as price (gross)"
|
--> "Store as price (gross)"
|
||||||
|
|
||||||
@enduml
|
@enduml
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
sphinx==9.1.*
|
sphinx==7.4.*
|
||||||
sphinx-rtd-theme~=3.1.0
|
jinja2==3.1.*
|
||||||
sphinxcontrib-httpdomain~=2.0.0
|
sphinx-rtd-theme
|
||||||
sphinxcontrib-images~=1.0.1
|
sphinxcontrib-httpdomain
|
||||||
sphinxcontrib-jquery~=4.1
|
sphinxcontrib-images
|
||||||
sphinxcontrib-spelling~=8.0.2
|
sphinxcontrib-jquery
|
||||||
sphinxemoji~=0.3.2
|
sphinxcontrib-spelling==8.*
|
||||||
pyenchant==3.3.*
|
sphinxemoji
|
||||||
|
pyenchant==3.2.*
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
-e ../
|
-e ../
|
||||||
sphinx==9.1.*
|
sphinx==7.4.*
|
||||||
sphinx-rtd-theme~=3.1.0
|
jinja2==3.1.*
|
||||||
sphinxcontrib-httpdomain~=2.0.0
|
sphinx-rtd-theme
|
||||||
sphinxcontrib-images~=1.0.1
|
sphinxcontrib-httpdomain
|
||||||
sphinxcontrib-jquery~=4.1
|
sphinxcontrib-images
|
||||||
sphinxcontrib-spelling~=8.0.2
|
sphinxcontrib-jquery
|
||||||
sphinxemoji~=0.3.2
|
sphinxcontrib-spelling==8.*
|
||||||
pyenchant==3.3.*
|
sphinxemoji
|
||||||
|
pyenchant==3.2.*
|
||||||
|
|||||||
+39
-40
@@ -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.11"
|
requires-python = ">=3.9"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
keywords = ["tickets", "web", "shop", "ecommerce"]
|
keywords = ["tickets", "web", "shop", "ecommerce"]
|
||||||
authors = [
|
authors = [
|
||||||
@@ -19,54 +19,52 @@ classifiers = [
|
|||||||
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
||||||
"Environment :: Web Environment",
|
"Environment :: Web Environment",
|
||||||
"License :: OSI Approved :: GNU Affero General Public License v3",
|
"License :: OSI Approved :: GNU Affero General Public License v3",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Framework :: Django :: 4.2",
|
||||||
"Programming Language :: Python :: 3.13",
|
|
||||||
"Programming Language :: Python :: 3.14",
|
|
||||||
"Framework :: Django :: 5.2",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
dependencies = [
|
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.13.*",
|
||||||
"bleach==6.3.*",
|
"bleach==6.2.*",
|
||||||
"celery==5.6.*",
|
"celery==5.5.*",
|
||||||
"chardet==5.2.*",
|
"chardet==5.2.*",
|
||||||
"cryptography>=44.0.0",
|
"cryptography>=44.0.0",
|
||||||
"css-inline==0.20.*",
|
"css-inline==0.15.*",
|
||||||
"defusedcsv>=1.1.0",
|
"defusedcsv>=1.1.0",
|
||||||
"dnspython==2.*",
|
"Django[argon2]==4.2.*,>=4.2.15",
|
||||||
"Django[argon2]==5.2.*",
|
"django-bootstrap3==25.1",
|
||||||
"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.5",
|
"django-formset-js-improved==0.5.0.3",
|
||||||
"django-formtools==2.5.1",
|
"django-formtools==2.5.1",
|
||||||
"django-hierarkey==2.0.*,>=2.0.1",
|
"django-hierarkey==1.2.*",
|
||||||
"django-hijack==3.7.*",
|
"django-hijack==3.7.*",
|
||||||
"django-i18nfield==1.11.*",
|
"django-i18nfield==1.10.*",
|
||||||
"django-libsass==0.9",
|
"django-libsass==0.9",
|
||||||
"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.7.*",
|
"django-otp==1.6.*",
|
||||||
"django-phonenumber-field==8.4.*",
|
"django-phonenumber-field==7.3.*",
|
||||||
"django-redis==6.0.*",
|
"django-redis==6.0.*",
|
||||||
"django-scopes==2.0.*",
|
"django-scopes==2.0.*",
|
||||||
"django-statici18n==2.7.*",
|
"django-statici18n==2.6.*",
|
||||||
"djangorestframework==3.16.*",
|
"djangorestframework==3.16.*",
|
||||||
"dnspython==2.8.*",
|
"dnspython==2.7.*",
|
||||||
"drf_ujson2==1.7.*",
|
"drf_ujson2==1.7.*",
|
||||||
"geoip2==5.*",
|
"geoip2==5.*",
|
||||||
"importlib_metadata==9.*", # 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+
|
||||||
"isoweek",
|
"isoweek",
|
||||||
"jsonschema",
|
"jsonschema",
|
||||||
"kombu==5.6.*",
|
"kombu==5.5.*",
|
||||||
"libsass==0.23.*",
|
"libsass==0.23.*",
|
||||||
"lxml",
|
"lxml",
|
||||||
"markdown==3.10.2", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
"markdown==3.8.2", # 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.*",
|
||||||
@@ -74,57 +72,58 @@ dependencies = [
|
|||||||
"packaging",
|
"packaging",
|
||||||
"paypalrestsdk==1.13.*",
|
"paypalrestsdk==1.13.*",
|
||||||
"paypal-checkout-serversdk==1.0.*",
|
"paypal-checkout-serversdk==1.0.*",
|
||||||
"PyJWT==2.12.*",
|
"PyJWT==2.10.*",
|
||||||
"phonenumberslite==9.0.*",
|
"phonenumberslite==9.0.*",
|
||||||
"Pillow==12.2.*",
|
"Pillow==11.2.*",
|
||||||
"pretix-plugin-build",
|
"pretix-plugin-build",
|
||||||
"protobuf==7.34.*",
|
"protobuf==6.31.*",
|
||||||
"psycopg2-binary",
|
"psycopg2-binary",
|
||||||
"pycountry",
|
"pycountry",
|
||||||
"pycparser==3.0",
|
"pycparser==2.22",
|
||||||
"pycryptodome==3.23.*",
|
"pycryptodome==3.23.*",
|
||||||
"pypdf==6.5.*",
|
"pypdf==5.6.*",
|
||||||
"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==7.4.*",
|
"redis==6.2.*",
|
||||||
"reportlab==4.4.*",
|
"reportlab==4.4.*",
|
||||||
"requests==2.32.*",
|
"requests==2.31.*",
|
||||||
"sentry-sdk==2.57.*",
|
"sentry-sdk==2.31.*",
|
||||||
"sepaxml==2.7.*",
|
"sepaxml==2.6.*",
|
||||||
"stripe==7.9.*",
|
"stripe==7.9.*",
|
||||||
"text-unidecode==1.*",
|
"text-unidecode==1.*",
|
||||||
"tlds>=2020041600",
|
"tlds>=2020041600",
|
||||||
"tqdm==4.*",
|
"tqdm==4.*",
|
||||||
"ua-parser==1.0.*",
|
"ua-parser==1.0.*",
|
||||||
|
"vat_moss_forked==2020.3.20.0.11.0",
|
||||||
"vobject==0.9.*",
|
"vobject==0.9.*",
|
||||||
"webauthn==2.7.*",
|
"webauthn==2.6.*",
|
||||||
"zeep==4.3.*"
|
"zeep==4.3.*"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
memcached = ["pylibmc"]
|
memcached = ["pylibmc"]
|
||||||
dev = [
|
dev = [
|
||||||
"aiohttp==3.13.*",
|
"aiohttp==3.12.*",
|
||||||
"coverage",
|
"coverage",
|
||||||
"coveralls",
|
"coveralls",
|
||||||
"fakeredis==2.34.*",
|
"fakeredis==2.30.*",
|
||||||
"flake8==7.3.*",
|
"flake8==7.3.*",
|
||||||
"freezegun",
|
"freezegun",
|
||||||
"isort==8.0.*",
|
"isort==6.0.*",
|
||||||
"pep8-naming==0.15.*",
|
"pep8-naming==0.15.*",
|
||||||
"potypo",
|
"potypo",
|
||||||
"pytest-asyncio>=0.24",
|
"pytest-asyncio>=0.24",
|
||||||
"pytest-cache",
|
"pytest-cache",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"pytest-django==4.*",
|
"pytest-django==4.*",
|
||||||
"pytest-mock==3.15.*",
|
"pytest-mock==3.14.*",
|
||||||
"pytest-sugar",
|
"pytest-sugar",
|
||||||
"pytest-xdist==3.8.*",
|
"pytest-xdist==3.7.*",
|
||||||
"pytest==9.0.*",
|
"pytest==8.4.*",
|
||||||
"responses",
|
"responses",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -25,8 +25,8 @@ coverage:
|
|||||||
coverage run -m py.test
|
coverage run -m py.test
|
||||||
|
|
||||||
npminstall:
|
npminstall:
|
||||||
# keep this in sync with pretix/_build.py!
|
# keep this in sync with setup.py!
|
||||||
mkdir -p pretix/static.dist/node_prefix/
|
mkdir -p pretix/static.dist/node_prefix/
|
||||||
cp -r pretix/static/npm_dir/* pretix/static.dist/node_prefix/
|
cp -r pretix/static/npm_dir/* pretix/static.dist/node_prefix/
|
||||||
npm ci --prefix=pretix/static.dist/node_prefix
|
npm install --prefix=pretix/static.dist/node_prefix
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -2,8 +2,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -19,4 +19,4 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# 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/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2026.4.0.dev0"
|
__version__ = "2025.7.0.dev0"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -115,7 +115,6 @@ ALL_LANGUAGES = [
|
|||||||
('sk', _('Slovak')),
|
('sk', _('Slovak')),
|
||||||
('sv', _('Swedish')),
|
('sv', _('Swedish')),
|
||||||
('es', _('Spanish')),
|
('es', _('Spanish')),
|
||||||
('es-419', _('Spanish (Latin America)')),
|
|
||||||
('tr', _('Turkish')),
|
('tr', _('Turkish')),
|
||||||
('uk', _('Ukrainian')),
|
('uk', _('Ukrainian')),
|
||||||
]
|
]
|
||||||
@@ -173,12 +172,6 @@ EXTRA_LANG_INFO = {
|
|||||||
'name': 'Norwegian Bokmal',
|
'name': 'Norwegian Bokmal',
|
||||||
'name_local': 'norsk (bokmål)',
|
'name_local': 'norsk (bokmål)',
|
||||||
},
|
},
|
||||||
'es-419': {
|
|
||||||
'bidi': False,
|
|
||||||
'code': 'es-419',
|
|
||||||
'name': 'Spanish (Latin America)',
|
|
||||||
'name_local': 'Español',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
django.conf.locale.LANG_INFO.update(EXTRA_LANG_INFO)
|
django.conf.locale.LANG_INFO.update(EXTRA_LANG_INFO)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -39,7 +39,7 @@ def npm_install():
|
|||||||
node_prefix = os.path.join(here, 'static.dist', 'node_prefix')
|
node_prefix = os.path.join(here, 'static.dist', 'node_prefix')
|
||||||
os.makedirs(node_prefix, exist_ok=True)
|
os.makedirs(node_prefix, exist_ok=True)
|
||||||
shutil.copytree(os.path.join(here, 'static', 'npm_dir'), node_prefix, dirs_exist_ok=True)
|
shutil.copytree(os.path.join(here, 'static', 'npm_dir'), node_prefix, dirs_exist_ok=True)
|
||||||
subprocess.check_call('npm ci', shell=True, cwd=node_prefix)
|
subprocess.check_call('npm install', shell=True, cwd=node_prefix)
|
||||||
npm_installed = True
|
npm_installed = True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -36,9 +36,7 @@ from rest_framework.permissions import SAFE_METHODS, BasePermission
|
|||||||
|
|
||||||
from pretix.api.models import OAuthAccessToken
|
from pretix.api.models import OAuthAccessToken
|
||||||
from pretix.base.models import Device, Event, User
|
from pretix.base.models import Device, Event, User
|
||||||
from pretix.base.models.auth import (
|
from pretix.base.models.auth import SuperuserPermissionSet
|
||||||
EventPermissionSet, OrganizerPermissionSet, SuperuserPermissionSet,
|
|
||||||
)
|
|
||||||
from pretix.base.models.organizer import TeamAPIToken
|
from pretix.base.models.organizer import TeamAPIToken
|
||||||
from pretix.helpers.security import (
|
from pretix.helpers.security import (
|
||||||
Session2FASetupRequired, SessionInvalid, SessionPasswordChangeRequired,
|
Session2FASetupRequired, SessionInvalid, SessionPasswordChangeRequired,
|
||||||
@@ -87,7 +85,7 @@ class EventPermission(BasePermission):
|
|||||||
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
|
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
|
||||||
request.eventpermset = SuperuserPermissionSet()
|
request.eventpermset = SuperuserPermissionSet()
|
||||||
else:
|
else:
|
||||||
request.eventpermset = EventPermissionSet(perm_holder.get_event_permission_set(request.organizer, request.event))
|
request.eventpermset = perm_holder.get_event_permission_set(request.organizer, request.event)
|
||||||
|
|
||||||
if isinstance(required_permission, (list, tuple)):
|
if isinstance(required_permission, (list, tuple)):
|
||||||
if not any(p in request.eventpermset for p in required_permission):
|
if not any(p in request.eventpermset for p in required_permission):
|
||||||
@@ -102,7 +100,7 @@ class EventPermission(BasePermission):
|
|||||||
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
|
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
|
||||||
request.orgapermset = SuperuserPermissionSet()
|
request.orgapermset = SuperuserPermissionSet()
|
||||||
else:
|
else:
|
||||||
request.orgapermset = OrganizerPermissionSet(perm_holder.get_organizer_permission_set(request.organizer))
|
request.orgapermset = perm_holder.get_organizer_permission_set(request.organizer)
|
||||||
|
|
||||||
if isinstance(required_permission, (list, tuple)):
|
if isinstance(required_permission, (list, tuple)):
|
||||||
if not any(p in request.eventpermset for p in required_permission):
|
if not any(p in request.eventpermset for p in required_permission):
|
||||||
@@ -126,12 +124,12 @@ class EventCRUDPermission(EventPermission):
|
|||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
if not super(EventCRUDPermission, self).has_permission(request, view):
|
if not super(EventCRUDPermission, self).has_permission(request, view):
|
||||||
return False
|
return False
|
||||||
elif view.action == 'create' and 'organizer.events:create' not in request.orgapermset:
|
elif view.action == 'create' and 'can_create_events' not in request.orgapermset:
|
||||||
return False
|
return False
|
||||||
elif view.action == 'destroy' and 'event.settings.general:write' not in request.eventpermset:
|
elif view.action == 'destroy' and 'can_change_event_settings' not in request.eventpermset:
|
||||||
return False
|
return False
|
||||||
elif view.action in ['update', 'partial_update'] \
|
elif view.action in ['update', 'partial_update'] \
|
||||||
and 'event.settings.general:write' not in request.eventpermset:
|
and 'can_change_event_settings' not in request.eventpermset:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
# Generated by Django 4.2.24 on 2025-11-14 16:21
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("pretixapi", "0013_alter_webhookcallretry_retry_not_before"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="webhook",
|
|
||||||
name="target_url",
|
|
||||||
field=models.URLField(max_length=1024),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="webhookcall",
|
|
||||||
name="target_url",
|
|
||||||
field=models.URLField(max_length=1024),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -114,7 +114,7 @@ class OAuthRefreshToken(AbstractRefreshToken):
|
|||||||
class WebHook(models.Model):
|
class WebHook(models.Model):
|
||||||
organizer = models.ForeignKey('pretixbase.Organizer', on_delete=models.CASCADE, related_name='webhooks')
|
organizer = models.ForeignKey('pretixbase.Organizer', on_delete=models.CASCADE, related_name='webhooks')
|
||||||
enabled = models.BooleanField(default=True, verbose_name=_("Enable webhook"))
|
enabled = models.BooleanField(default=True, verbose_name=_("Enable webhook"))
|
||||||
target_url = models.URLField(verbose_name=_("Target URL"), max_length=1024)
|
target_url = models.URLField(verbose_name=_("Target URL"), max_length=255)
|
||||||
all_events = models.BooleanField(default=True, verbose_name=_("All events (including newly created ones)"))
|
all_events = models.BooleanField(default=True, verbose_name=_("All events (including newly created ones)"))
|
||||||
limit_events = models.ManyToManyField('pretixbase.Event', verbose_name=_("Limit to events"), blank=True)
|
limit_events = models.ManyToManyField('pretixbase.Event', verbose_name=_("Limit to events"), blank=True)
|
||||||
comment = models.CharField(verbose_name=_("Comment"), max_length=255, null=True, blank=True)
|
comment = models.CharField(verbose_name=_("Comment"), max_length=255, null=True, blank=True)
|
||||||
@@ -140,7 +140,7 @@ class WebHookEventListener(models.Model):
|
|||||||
class WebHookCall(models.Model):
|
class WebHookCall(models.Model):
|
||||||
webhook = models.ForeignKey('WebHook', on_delete=models.CASCADE, related_name='calls')
|
webhook = models.ForeignKey('WebHook', on_delete=models.CASCADE, related_name='calls')
|
||||||
datetime = models.DateTimeField(auto_now_add=True)
|
datetime = models.DateTimeField(auto_now_add=True)
|
||||||
target_url = models.URLField(max_length=1024)
|
target_url = models.URLField(max_length=255)
|
||||||
action_type = models.CharField(max_length=255)
|
action_type = models.CharField(max_length=255)
|
||||||
is_retry = models.BooleanField(default=False)
|
is_retry = models.BooleanField(default=False)
|
||||||
execution_time = models.FloatField(null=True)
|
execution_time = models.FloatField(null=True)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -104,14 +104,3 @@ class MiniCheckinListSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class CheckinRPCAnnulInputSerializer(serializers.Serializer):
|
|
||||||
lists = serializers.PrimaryKeyRelatedField(required=True, many=True, queryset=CheckinList.objects.none())
|
|
||||||
nonce = serializers.CharField(required=True, allow_null=False)
|
|
||||||
datetime = serializers.DateTimeField(required=False, allow_null=True)
|
|
||||||
error_explanation = serializers.CharField(required=False, allow_null=True)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields['lists'].child_relation.queryset = CheckinList.objects.filter(event__in=self.context['events']).select_related('event')
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -50,7 +50,6 @@ from rest_framework.relations import SlugRelatedField
|
|||||||
from pretix.api.serializers import (
|
from pretix.api.serializers import (
|
||||||
CompatibleJSONField, SalesChannelMigrationMixin,
|
CompatibleJSONField, SalesChannelMigrationMixin,
|
||||||
)
|
)
|
||||||
from pretix.api.serializers.fields import PluginsField
|
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.settings import SettingsSerializer
|
from pretix.api.serializers.settings import SettingsSerializer
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
@@ -62,9 +61,6 @@ from pretix.base.models.items import (
|
|||||||
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
||||||
)
|
)
|
||||||
from pretix.base.models.tax import CustomRulesValidator
|
from pretix.base.models.tax import CustomRulesValidator
|
||||||
from pretix.base.plugins import (
|
|
||||||
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
|
||||||
)
|
|
||||||
from pretix.base.services.seating import (
|
from pretix.base.services.seating import (
|
||||||
SeatProtected, generate_seats, validate_plan_change,
|
SeatProtected, generate_seats, validate_plan_change,
|
||||||
)
|
)
|
||||||
@@ -130,6 +126,22 @@ class SeatCategoryMappingField(Field):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PluginsField(Field):
|
||||||
|
|
||||||
|
def to_representation(self, obj):
|
||||||
|
from pretix.base.plugins import get_all_plugins
|
||||||
|
|
||||||
|
return sorted([
|
||||||
|
p.module for p in get_all_plugins()
|
||||||
|
if not p.name.startswith('.') and getattr(p, 'visible', True) and p.module in obj.get_plugins()
|
||||||
|
])
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
return {
|
||||||
|
'plugins': data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TimeZoneField(ChoiceField):
|
class TimeZoneField(ChoiceField):
|
||||||
def get_attribute(self, instance):
|
def get_attribute(self, instance):
|
||||||
return instance.cache.get_or_set(
|
return instance.cache.get_or_set(
|
||||||
@@ -271,28 +283,17 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
from pretix.base.plugins import get_all_plugins
|
from pretix.base.plugins import get_all_plugins
|
||||||
|
|
||||||
plugins_available = {
|
plugins_available = {
|
||||||
p.module: p for p in get_all_plugins(event=self.instance)
|
p.module: p for p in get_all_plugins(self.instance)
|
||||||
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
||||||
}
|
}
|
||||||
current_plugins = self.instance.get_plugins() if self.instance and self.instance.pk else []
|
|
||||||
settings_holder = self.instance if self.instance and self.instance.pk else self.context['organizer']
|
settings_holder = self.instance if self.instance and self.instance.pk else self.context['organizer']
|
||||||
|
|
||||||
allowed_levels = (PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID)
|
|
||||||
for plugin in value.get('plugins'):
|
for plugin in value.get('plugins'):
|
||||||
if plugin not in plugins_available:
|
if plugin not in plugins_available:
|
||||||
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
|
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
|
||||||
if getattr(plugins_available[plugin], 'restricted', False):
|
if getattr(plugins_available[plugin], 'restricted', False):
|
||||||
if plugin not in settings_holder.settings.allowed_restricted_plugins:
|
if plugin not in settings_holder.settings.allowed_restricted_plugins:
|
||||||
raise ValidationError(_('Restricted plugin: \'{name}\'.').format(name=plugin))
|
raise ValidationError(_('Restricted plugin: \'{name}\'.').format(name=plugin))
|
||||||
level = getattr(plugins_available[plugin], 'level', PLUGIN_LEVEL_EVENT)
|
|
||||||
if level not in allowed_levels:
|
|
||||||
raise ValidationError('Plugin cannot be enabled on this level: \'{name}\'.'.format(name=plugin))
|
|
||||||
|
|
||||||
if level == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID and plugin not in self.context['organizer'].get_plugins():
|
|
||||||
if plugin not in current_plugins:
|
|
||||||
# Technically, this is allowed, but consumers might be confused if the API call doesn't do anything
|
|
||||||
# so we prevent this change.
|
|
||||||
raise ValidationError('Plugin should be enabled on organizer level first: \'{name}\'.'.format(name=plugin))
|
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@@ -300,7 +301,7 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
def ignored_meta_properties(self):
|
def ignored_meta_properties(self):
|
||||||
perm_holder = (self.context['request'].auth if isinstance(self.context['request'].auth, (Device, TeamAPIToken))
|
perm_holder = (self.context['request'].auth if isinstance(self.context['request'].auth, (Device, TeamAPIToken))
|
||||||
else self.context['request'].user)
|
else self.context['request'].user)
|
||||||
if perm_holder.has_organizer_permission(self.context['request'].organizer, 'organizer.settings.general:write', request=self.context['request']):
|
if perm_holder.has_organizer_permission(self.context['request'].organizer, 'can_change_organizer_settings', request=self.context['request']):
|
||||||
return []
|
return []
|
||||||
return [k for k, p in self.meta_properties.items() if p.protected]
|
return [k for k, p in self.meta_properties.items() if p.protected]
|
||||||
|
|
||||||
@@ -445,7 +446,7 @@ class CloneEventSerializer(EventSerializer):
|
|||||||
date_admission = validated_data.pop('date_admission', None)
|
date_admission = validated_data.pop('date_admission', None)
|
||||||
new_event = super().create({**validated_data, 'plugins': None})
|
new_event = super().create({**validated_data, 'plugins': None})
|
||||||
|
|
||||||
event = self.context['event']
|
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
|
||||||
new_event.copy_data_from(event, skip_meta_data='meta_data' in validated_data)
|
new_event.copy_data_from(event, skip_meta_data='meta_data' in validated_data)
|
||||||
|
|
||||||
if plugins is not None:
|
if plugins is not None:
|
||||||
@@ -561,7 +562,7 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
|||||||
def ignored_meta_properties(self):
|
def ignored_meta_properties(self):
|
||||||
perm_holder = (self.context['request'].auth if isinstance(self.context['request'].auth, (Device, TeamAPIToken))
|
perm_holder = (self.context['request'].auth if isinstance(self.context['request'].auth, (Device, TeamAPIToken))
|
||||||
else self.context['request'].user)
|
else self.context['request'].user)
|
||||||
if perm_holder.has_organizer_permission(self.context['request'].organizer, 'organizer.settings.general:write', request=self.context['request']):
|
if perm_holder.has_organizer_permission(self.context['request'].organizer, 'can_change_organizer_settings', request=self.context['request']):
|
||||||
return []
|
return []
|
||||||
return [k for k, p in self.meta_properties.items() if p.protected]
|
return [k for k, p in self.meta_properties.items() if p.protected]
|
||||||
|
|
||||||
@@ -684,33 +685,12 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TaxRule
|
model = TaxRule
|
||||||
fields = ('id', 'name', 'default', 'rate', 'code', 'price_includes_tax', 'eu_reverse_charge', 'home_country',
|
fields = ('id', 'name', 'rate', 'code', 'price_includes_tax', 'eu_reverse_charge', 'home_country',
|
||||||
'internal_name', 'keep_gross_if_rate_changes', 'custom_rules', 'default')
|
'internal_name', 'keep_gross_if_rate_changes', 'custom_rules')
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
if "default" not in validated_data and not self.context["event"].tax_rules.exists():
|
|
||||||
validated_data["default"] = True
|
|
||||||
return super().create(validated_data)
|
|
||||||
|
|
||||||
def save(self, **kwargs):
|
|
||||||
if self.validated_data.get("default"):
|
|
||||||
if self.instance and self.instance.pk:
|
|
||||||
self.context["event"].tax_rules.exclude(pk=self.instance.pk).update(default=False)
|
|
||||||
else:
|
|
||||||
self.context["event"].tax_rules.update(default=False)
|
|
||||||
return super().save(**kwargs)
|
|
||||||
|
|
||||||
def validate_default(self, value):
|
|
||||||
if not value and self.instance.default:
|
|
||||||
raise ValidationError("You can't remove the default property, instead set it on another tax rule.")
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class EventSettingsSerializer(SettingsSerializer):
|
class EventSettingsSerializer(SettingsSerializer):
|
||||||
default_write_permission = 'event.settings.general:write'
|
|
||||||
default_fields = [
|
default_fields = [
|
||||||
# These are readable for all users with access to the events, therefore secrets stored in the settings store
|
|
||||||
# should not be included!
|
|
||||||
'imprint_url',
|
'imprint_url',
|
||||||
'checkout_email_helptext',
|
'checkout_email_helptext',
|
||||||
'presale_has_ended_text',
|
'presale_has_ended_text',
|
||||||
@@ -732,8 +712,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'allow_modifications_after_checkin',
|
'allow_modifications_after_checkin',
|
||||||
'last_order_modification_date',
|
'last_order_modification_date',
|
||||||
'show_quota_left',
|
'show_quota_left',
|
||||||
'tax_rule_payment',
|
|
||||||
'tax_rule_cancellation',
|
|
||||||
'waiting_list_enabled',
|
'waiting_list_enabled',
|
||||||
'waiting_list_auto_disable',
|
'waiting_list_auto_disable',
|
||||||
'waiting_list_hours',
|
'waiting_list_hours',
|
||||||
@@ -798,7 +776,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'invoice_address_asked',
|
'invoice_address_asked',
|
||||||
'invoice_address_required',
|
'invoice_address_required',
|
||||||
'invoice_address_vatid',
|
'invoice_address_vatid',
|
||||||
'invoice_address_vatid_required_countries',
|
|
||||||
'invoice_address_company_required',
|
'invoice_address_company_required',
|
||||||
'invoice_address_beneficiary',
|
'invoice_address_beneficiary',
|
||||||
'invoice_address_custom_field',
|
'invoice_address_custom_field',
|
||||||
@@ -809,8 +786,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'invoice_reissue_after_modify',
|
'invoice_reissue_after_modify',
|
||||||
'invoice_include_free',
|
'invoice_include_free',
|
||||||
'invoice_generate',
|
'invoice_generate',
|
||||||
'invoice_generate_only_business',
|
|
||||||
'invoice_period',
|
|
||||||
'invoice_numbers_consecutive',
|
'invoice_numbers_consecutive',
|
||||||
'invoice_numbers_prefix',
|
'invoice_numbers_prefix',
|
||||||
'invoice_numbers_prefix_cancellations',
|
'invoice_numbers_prefix_cancellations',
|
||||||
@@ -825,7 +800,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'invoice_address_from',
|
'invoice_address_from',
|
||||||
'invoice_address_from_zipcode',
|
'invoice_address_from_zipcode',
|
||||||
'invoice_address_from_city',
|
'invoice_address_from_city',
|
||||||
'invoice_address_from_state',
|
|
||||||
'invoice_address_from_country',
|
'invoice_address_from_country',
|
||||||
'invoice_address_from_tax_id',
|
'invoice_address_from_tax_id',
|
||||||
'invoice_address_from_vat_id',
|
'invoice_address_from_vat_id',
|
||||||
@@ -835,7 +809,6 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'invoice_eu_currencies',
|
'invoice_eu_currencies',
|
||||||
'invoice_logo_image',
|
'invoice_logo_image',
|
||||||
'invoice_renderer_highlight_order_code',
|
'invoice_renderer_highlight_order_code',
|
||||||
'tax_rounding',
|
|
||||||
'cancel_allow_user',
|
'cancel_allow_user',
|
||||||
'cancel_allow_user_until',
|
'cancel_allow_user_until',
|
||||||
'cancel_allow_user_unpaid_keep',
|
'cancel_allow_user_unpaid_keep',
|
||||||
@@ -948,7 +921,6 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
|||||||
'invoice_address_asked',
|
'invoice_address_asked',
|
||||||
'invoice_address_required',
|
'invoice_address_required',
|
||||||
'invoice_address_vatid',
|
'invoice_address_vatid',
|
||||||
'invoice_address_vatid_required_countries',
|
|
||||||
'invoice_address_company_required',
|
'invoice_address_company_required',
|
||||||
'invoice_address_beneficiary',
|
'invoice_address_beneficiary',
|
||||||
'invoice_address_custom_field',
|
'invoice_address_custom_field',
|
||||||
@@ -959,7 +931,6 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
|||||||
'invoice_address_from',
|
'invoice_address_from',
|
||||||
'invoice_address_from_zipcode',
|
'invoice_address_from_zipcode',
|
||||||
'invoice_address_from_city',
|
'invoice_address_from_city',
|
||||||
'invoice_address_from_state',
|
|
||||||
'invoice_address_from_country',
|
'invoice_address_from_country',
|
||||||
'invoice_address_from_tax_id',
|
'invoice_address_from_tax_id',
|
||||||
'invoice_address_from_vat_id',
|
'invoice_address_from_vat_id',
|
||||||
@@ -971,8 +942,6 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
|||||||
'reusable_media_type_nfc_mf0aes',
|
'reusable_media_type_nfc_mf0aes',
|
||||||
'reusable_media_type_nfc_mf0aes_random_uid',
|
'reusable_media_type_nfc_mf0aes_random_uid',
|
||||||
'system_question_order',
|
'system_question_order',
|
||||||
'tax_rule_payment',
|
|
||||||
'tax_rule_cancellation',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -1083,16 +1052,16 @@ class SeatSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
def prefetch_expanded_data(self, items, request, expand_fields):
|
def prefetch_expanded_data(self, items, request, expand_fields):
|
||||||
if 'orderposition' in expand_fields:
|
if 'orderposition' in expand_fields:
|
||||||
if 'event.orders:read' not in request.eventpermset:
|
if 'can_view_orders' not in request.eventpermset:
|
||||||
raise PermissionDenied('event.orders:read permission required for expand=orderposition')
|
raise PermissionDenied('can_view_orders permission required for expand=orderposition')
|
||||||
prefetch_by_id(items, OrderPosition.objects.prefetch_related('order'), 'orderposition_id', 'orderposition')
|
prefetch_by_id(items, OrderPosition.objects.prefetch_related('order'), 'orderposition_id', 'orderposition')
|
||||||
if 'cartposition' in expand_fields:
|
if 'cartposition' in expand_fields:
|
||||||
if 'event.orders:read' not in request.eventpermset:
|
if 'can_view_orders' not in request.eventpermset:
|
||||||
raise PermissionDenied('event.orders:read permission required for expand=cartposition')
|
raise PermissionDenied('can_view_orders permission required for expand=cartposition')
|
||||||
prefetch_by_id(items, CartPosition.objects, 'cartposition_id', 'cartposition')
|
prefetch_by_id(items, CartPosition.objects, 'cartposition_id', 'cartposition')
|
||||||
if 'voucher' in expand_fields:
|
if 'voucher' in expand_fields:
|
||||||
if 'event.vouchers:read' not in request.eventpermset:
|
if 'can_view_vouchers' not in request.eventpermset:
|
||||||
raise PermissionDenied('event.vouchers:read permission required for expand=voucher')
|
raise PermissionDenied('can_view_vouchers permission required for expand=voucher')
|
||||||
prefetch_by_id(items, Voucher.objects, 'voucher_id', 'voucher')
|
prefetch_by_id(items, Voucher.objects, 'voucher_id', 'voucher')
|
||||||
|
|
||||||
def __init__(self, instance, *args, **kwargs):
|
def __init__(self, instance, *args, **kwargs):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -19,18 +19,45 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# 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/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import QueryDict
|
from django.http import QueryDict
|
||||||
from pytz import common_timezones
|
from pytz import common_timezones
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
from pretix.api.serializers.forms import form_field_to_serializer_field
|
|
||||||
from pretix.base.exporter import OrganizerLevelExportMixin
|
from pretix.base.exporter import OrganizerLevelExportMixin
|
||||||
from pretix.base.models import (
|
from pretix.base.models import ScheduledEventExport, ScheduledOrganizerExport
|
||||||
Event, ScheduledEventExport, ScheduledOrganizerExport,
|
from pretix.base.timeframes import DateFrameField, SerializerDateFrameField
|
||||||
|
|
||||||
|
|
||||||
|
class FormFieldWrapperField(serializers.Field):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.form_field = kwargs.pop('form_field')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
return self.form_field.widget.format_value(value)
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
d = self.form_field.widget.value_from_datadict({'name': data}, {}, 'name')
|
||||||
|
d = self.form_field.clean(d)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
simple_mappings = (
|
||||||
|
(forms.DateField, serializers.DateField, ()),
|
||||||
|
(forms.TimeField, serializers.TimeField, ()),
|
||||||
|
(forms.SplitDateTimeField, serializers.DateTimeField, ()),
|
||||||
|
(forms.DateTimeField, serializers.DateTimeField, ()),
|
||||||
|
(forms.DecimalField, serializers.DecimalField, ('max_digits', 'decimal_places', 'min_value', 'max_value')),
|
||||||
|
(forms.FloatField, serializers.FloatField, ()),
|
||||||
|
(forms.IntegerField, serializers.IntegerField, ()),
|
||||||
|
(forms.EmailField, serializers.EmailField, ()),
|
||||||
|
(forms.UUIDField, serializers.UUIDField, ()),
|
||||||
|
(forms.URLField, serializers.URLField, ()),
|
||||||
|
(forms.BooleanField, serializers.BooleanField, ()),
|
||||||
)
|
)
|
||||||
from pretix.base.timeframes import SerializerDateFrameField
|
|
||||||
|
|
||||||
|
|
||||||
class SerializerDescriptionField(serializers.Field):
|
class SerializerDescriptionField(serializers.Field):
|
||||||
@@ -54,30 +81,80 @@ class ExporterSerializer(serializers.Serializer):
|
|||||||
input_parameters = SerializerDescriptionField(source='_serializer')
|
input_parameters = SerializerDescriptionField(source='_serializer')
|
||||||
|
|
||||||
|
|
||||||
|
class PrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
|
||||||
|
def to_representation(self, value):
|
||||||
|
if isinstance(value, int):
|
||||||
|
return value
|
||||||
|
return super().to_representation(value)
|
||||||
|
|
||||||
|
|
||||||
class JobRunSerializer(serializers.Serializer):
|
class JobRunSerializer(serializers.Serializer):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
ex = self.ex = kwargs.pop('exporter')
|
ex = kwargs.pop('exporter')
|
||||||
|
events = kwargs.pop('events', None)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if ex.is_multievent and not isinstance(ex, OrganizerLevelExportMixin):
|
if events is not None and not isinstance(ex, OrganizerLevelExportMixin):
|
||||||
self.fields["all_events"] = serializers.BooleanField(
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
self.fields["events"] = serializers.SlugRelatedField(
|
self.fields["events"] = serializers.SlugRelatedField(
|
||||||
queryset=ex.events,
|
queryset=events,
|
||||||
required=False,
|
required=False,
|
||||||
allow_empty=True,
|
allow_empty=False,
|
||||||
slug_field='slug',
|
slug_field='slug',
|
||||||
many=True
|
many=True
|
||||||
)
|
)
|
||||||
for k, v in ex.export_form_fields.items():
|
for k, v in ex.export_form_fields.items():
|
||||||
self.fields[k] = form_field_to_serializer_field(v)
|
for m_from, m_to, m_kwargs in simple_mappings:
|
||||||
|
if isinstance(v, m_from):
|
||||||
|
self.fields[k] = m_to(
|
||||||
|
required=v.required,
|
||||||
|
allow_null=not v.required,
|
||||||
|
validators=v.validators,
|
||||||
|
**{kwarg: getattr(v, kwargs, None) for kwarg in m_kwargs}
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
def to_representation(self, instance):
|
if isinstance(v, forms.NullBooleanField):
|
||||||
# Translate between events as a list of slugs (API) and list of ints (database)
|
self.fields[k] = serializers.BooleanField(
|
||||||
if self.ex.is_multievent and not isinstance(self.ex, OrganizerLevelExportMixin) and "events" in instance and isinstance(instance["events"], list):
|
required=v.required,
|
||||||
instance["events"] = [e for e in self.ex.events.filter(pk__in=instance["events"])]
|
allow_null=True,
|
||||||
instance = super().to_representation(instance)
|
validators=v.validators,
|
||||||
return instance
|
)
|
||||||
|
if isinstance(v, forms.ModelMultipleChoiceField):
|
||||||
|
self.fields[k] = PrimaryKeyRelatedField(
|
||||||
|
queryset=v.queryset,
|
||||||
|
required=v.required,
|
||||||
|
allow_empty=not v.required,
|
||||||
|
validators=v.validators,
|
||||||
|
many=True
|
||||||
|
)
|
||||||
|
elif isinstance(v, forms.ModelChoiceField):
|
||||||
|
self.fields[k] = PrimaryKeyRelatedField(
|
||||||
|
queryset=v.queryset,
|
||||||
|
required=v.required,
|
||||||
|
allow_null=not v.required,
|
||||||
|
validators=v.validators,
|
||||||
|
)
|
||||||
|
elif isinstance(v, forms.MultipleChoiceField):
|
||||||
|
self.fields[k] = serializers.MultipleChoiceField(
|
||||||
|
choices=v.choices,
|
||||||
|
required=v.required,
|
||||||
|
allow_empty=not v.required,
|
||||||
|
validators=v.validators,
|
||||||
|
)
|
||||||
|
elif isinstance(v, forms.ChoiceField):
|
||||||
|
self.fields[k] = serializers.ChoiceField(
|
||||||
|
choices=v.choices,
|
||||||
|
required=v.required,
|
||||||
|
allow_null=not v.required,
|
||||||
|
validators=v.validators,
|
||||||
|
)
|
||||||
|
elif isinstance(v, DateFrameField):
|
||||||
|
self.fields[k] = SerializerDateFrameField(
|
||||||
|
required=v.required,
|
||||||
|
allow_null=not v.required,
|
||||||
|
validators=v.validators,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.fields[k] = FormFieldWrapperField(form_field=v, required=v.required, allow_null=not v.required)
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
if isinstance(data, QueryDict):
|
if isinstance(data, QueryDict):
|
||||||
@@ -106,14 +183,6 @@ class JobRunSerializer(serializers.Serializer):
|
|||||||
data[fk] = f'{d_from.isoformat() if d_from else ""}/{d_to.isoformat() if d_to else ""}'
|
data[fk] = f'{d_from.isoformat() if d_from else ""}/{d_to.isoformat() if d_to else ""}'
|
||||||
|
|
||||||
data = super().to_internal_value(data)
|
data = super().to_internal_value(data)
|
||||||
|
|
||||||
# Translate between events as a list of slugs (API) and list of ints (database)
|
|
||||||
if self.ex.is_multievent and not isinstance(self.ex, OrganizerLevelExportMixin) and "events" in data and isinstance(data["events"], list):
|
|
||||||
if data["events"] and isinstance(data["events"][0], Event):
|
|
||||||
data["events"] = [e.pk for e in data["events"]]
|
|
||||||
elif data["events"] and isinstance(data["events"][0], str):
|
|
||||||
data["events"] = [e.pk for e in self.ex.events.filter(slug__in=data["events"]).only("pk")]
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def is_valid(self, raise_exception=False):
|
def is_valid(self, raise_exception=False):
|
||||||
@@ -150,20 +219,13 @@ class ScheduledExportSerializer(serializers.ModelSerializer):
|
|||||||
exporter = self.context['exporters'].get(identifier)
|
exporter = self.context['exporters'].get(identifier)
|
||||||
if exporter:
|
if exporter:
|
||||||
try:
|
try:
|
||||||
attrs["export_form_data"] = JobRunSerializer(exporter=exporter).to_internal_value(attrs["export_form_data"])
|
JobRunSerializer(exporter=exporter).to_internal_value(attrs["export_form_data"])
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
raise ValidationError({"export_form_data": e.detail})
|
raise ValidationError({"export_form_data": e.detail})
|
||||||
else:
|
else:
|
||||||
raise ValidationError({"export_identifier": ["Unknown exporter."]})
|
raise ValidationError({"export_identifier": ["Unknown exporter."]})
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def to_representation(self, instance):
|
|
||||||
repr = super().to_representation(instance)
|
|
||||||
exporter = self.context['exporters'].get(instance.export_identifier)
|
|
||||||
if exporter:
|
|
||||||
repr["export_form_data"] = JobRunSerializer(exporter=exporter).to_representation(repr["export_form_data"])
|
|
||||||
return repr
|
|
||||||
|
|
||||||
def validate_mail_additional_recipients(self, value):
|
def validate_mail_additional_recipients(self, value):
|
||||||
d = value.replace(' ', '')
|
d = value.replace(' ', '')
|
||||||
if len(d.split(',')) > 25:
|
if len(d.split(',')) > 25:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -109,19 +109,3 @@ class UploadedFileField(serializers.Field):
|
|||||||
return None
|
return None
|
||||||
request = self.context['request']
|
request = self.context['request']
|
||||||
return request.build_absolute_uri(url)
|
return request.build_absolute_uri(url)
|
||||||
|
|
||||||
|
|
||||||
class PluginsField(serializers.Field):
|
|
||||||
|
|
||||||
def to_representation(self, obj):
|
|
||||||
from pretix.base.plugins import get_all_plugins
|
|
||||||
|
|
||||||
return sorted([
|
|
||||||
p.module for p in get_all_plugins()
|
|
||||||
if not p.name.startswith('.') and getattr(p, 'visible', True) and p.module in obj.get_plugins()
|
|
||||||
])
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
return {
|
|
||||||
'plugins': data
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
#
|
|
||||||
# This file is part of pretix (Community Edition).
|
|
||||||
#
|
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
||||||
#
|
|
||||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
||||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
||||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
||||||
# this file, see <https://pretix.eu/about/en/license>.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
||||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
||||||
# <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
from django import forms
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from pretix.base.timeframes import DateFrameField, SerializerDateFrameField
|
|
||||||
|
|
||||||
simple_mappings = (
|
|
||||||
(forms.DateField, serializers.DateField, ()),
|
|
||||||
(forms.TimeField, serializers.TimeField, ()),
|
|
||||||
(forms.SplitDateTimeField, serializers.DateTimeField, ()),
|
|
||||||
(forms.DateTimeField, serializers.DateTimeField, ()),
|
|
||||||
(forms.DecimalField, serializers.DecimalField, ('max_digits', 'decimal_places', 'min_value', 'max_value')),
|
|
||||||
(forms.FloatField, serializers.FloatField, ()),
|
|
||||||
(forms.IntegerField, serializers.IntegerField, ()),
|
|
||||||
(forms.EmailField, serializers.EmailField, ()),
|
|
||||||
(forms.UUIDField, serializers.UUIDField, ()),
|
|
||||||
(forms.URLField, serializers.URLField, ()),
|
|
||||||
(forms.BooleanField, serializers.BooleanField, ()),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
|
|
||||||
def to_representation(self, value):
|
|
||||||
if isinstance(value, int):
|
|
||||||
return value
|
|
||||||
return super().to_representation(value)
|
|
||||||
|
|
||||||
|
|
||||||
class FormFieldWrapperField(serializers.Field):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.form_field = kwargs.pop('form_field')
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def to_representation(self, value):
|
|
||||||
return self.form_field.widget.format_value(value)
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
d = self.form_field.widget.value_from_datadict({'name': data}, {}, 'name')
|
|
||||||
d = self.form_field.clean(d)
|
|
||||||
return d
|
|
||||||
|
|
||||||
|
|
||||||
def form_field_to_serializer_field(field):
|
|
||||||
for m_from, m_to, m_kwargs in simple_mappings:
|
|
||||||
if isinstance(field, m_from):
|
|
||||||
return m_to(
|
|
||||||
required=field.required,
|
|
||||||
allow_null=not field.required and not isinstance(field, forms.BooleanField),
|
|
||||||
validators=field.validators,
|
|
||||||
initial=field.initial,
|
|
||||||
**{kwarg: getattr(field, kwarg, None) for kwarg in m_kwargs}
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(field, forms.NullBooleanField):
|
|
||||||
return serializers.BooleanField(
|
|
||||||
required=field.required,
|
|
||||||
allow_null=True,
|
|
||||||
validators=field.validators,
|
|
||||||
)
|
|
||||||
if isinstance(field, forms.ModelMultipleChoiceField):
|
|
||||||
return PrimaryKeyRelatedField(
|
|
||||||
queryset=field.queryset,
|
|
||||||
required=field.required,
|
|
||||||
allow_empty=not field.required,
|
|
||||||
validators=field.validators,
|
|
||||||
many=True
|
|
||||||
)
|
|
||||||
elif isinstance(field, forms.ModelChoiceField):
|
|
||||||
return PrimaryKeyRelatedField(
|
|
||||||
queryset=field.queryset,
|
|
||||||
required=field.required,
|
|
||||||
allow_null=not field.required,
|
|
||||||
validators=field.validators,
|
|
||||||
)
|
|
||||||
elif isinstance(field, forms.MultipleChoiceField):
|
|
||||||
return serializers.MultipleChoiceField(
|
|
||||||
choices=field.choices,
|
|
||||||
required=field.required,
|
|
||||||
allow_empty=not field.required,
|
|
||||||
validators=field.validators,
|
|
||||||
)
|
|
||||||
elif isinstance(field, forms.ChoiceField):
|
|
||||||
return serializers.ChoiceField(
|
|
||||||
choices=field.choices,
|
|
||||||
required=field.required,
|
|
||||||
allow_null=not field.required,
|
|
||||||
validators=field.validators,
|
|
||||||
)
|
|
||||||
elif isinstance(field, DateFrameField):
|
|
||||||
return SerializerDateFrameField(
|
|
||||||
required=field.required,
|
|
||||||
allow_null=not field.required,
|
|
||||||
validators=field.validators,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return FormFieldWrapperField(form_field=field, required=field.required, allow_null=not field.required)
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -47,9 +47,8 @@ from pretix.api.serializers.event import MetaDataField
|
|||||||
from pretix.api.serializers.fields import UploadedFileField
|
from pretix.api.serializers.fields import UploadedFileField
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemProgramTime,
|
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
|
||||||
ItemVariation, ItemVariationMetaValue, Question, QuestionOption, Quota,
|
ItemVariationMetaValue, Question, QuestionOption, Quota, SalesChannel,
|
||||||
SalesChannel,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -188,12 +187,6 @@ class InlineItemAddOnSerializer(serializers.ModelSerializer):
|
|||||||
'position', 'price_included', 'multi_allowed')
|
'position', 'price_included', 'multi_allowed')
|
||||||
|
|
||||||
|
|
||||||
class InlineItemProgramTimeSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = ItemProgramTime
|
|
||||||
fields = ('start', 'end')
|
|
||||||
|
|
||||||
|
|
||||||
class ItemBundleSerializer(serializers.ModelSerializer):
|
class ItemBundleSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ItemBundle
|
model = ItemBundle
|
||||||
@@ -219,37 +212,6 @@ class ItemBundleSerializer(serializers.ModelSerializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class ItemProgramTimeSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = ItemProgramTime
|
|
||||||
fields = ('id', 'start', 'end')
|
|
||||||
|
|
||||||
def validate(self, data):
|
|
||||||
data = super().validate(data)
|
|
||||||
|
|
||||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
|
||||||
full_data.update(data)
|
|
||||||
|
|
||||||
start = full_data.get('start')
|
|
||||||
if not start:
|
|
||||||
raise ValidationError(_("The program start must not be empty."))
|
|
||||||
|
|
||||||
end = full_data.get('end')
|
|
||||||
if not end:
|
|
||||||
raise ValidationError(_("The program end must not be empty."))
|
|
||||||
|
|
||||||
if start > end:
|
|
||||||
raise ValidationError(_("The program end must not be before the program start."))
|
|
||||||
|
|
||||||
event = self.context['event']
|
|
||||||
if event.has_subevents:
|
|
||||||
raise ValidationError({
|
|
||||||
_("You cannot use program times on an event series.")
|
|
||||||
})
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class ItemAddOnSerializer(serializers.ModelSerializer):
|
class ItemAddOnSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ItemAddOn
|
model = ItemAddOn
|
||||||
@@ -288,7 +250,6 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
addons = InlineItemAddOnSerializer(many=True, required=False)
|
addons = InlineItemAddOnSerializer(many=True, required=False)
|
||||||
bundles = InlineItemBundleSerializer(many=True, required=False)
|
bundles = InlineItemBundleSerializer(many=True, required=False)
|
||||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||||
program_times = InlineItemProgramTimeSerializer(many=True, required=False)
|
|
||||||
tax_rate = ItemTaxRateField(source='*', read_only=True)
|
tax_rate = ItemTaxRateField(source='*', read_only=True)
|
||||||
meta_data = MetaDataField(required=False, source='*')
|
meta_data = MetaDataField(required=False, source='*')
|
||||||
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
||||||
@@ -310,7 +271,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||||
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||||
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
|
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
|
||||||
'addons', 'bundles', 'program_times', 'original_price', 'require_approval', 'generate_tickets',
|
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
||||||
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'hidden_if_item_available_mode', 'allow_waitinglist',
|
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'hidden_if_item_available_mode', 'allow_waitinglist',
|
||||||
'issue_giftcard', 'meta_data',
|
'issue_giftcard', 'meta_data',
|
||||||
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
|
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
|
||||||
@@ -333,9 +294,9 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
if self.instance and ('addons' in data or 'variations' in data or 'bundles' in data or 'program_times' in data):
|
if self.instance and ('addons' in data or 'variations' in data or 'bundles' in data):
|
||||||
raise ValidationError(_('Updating add-ons, bundles, program times or variations via PATCH/PUT is not '
|
raise ValidationError(_('Updating add-ons, bundles, or variations via PATCH/PUT is not supported. Please use the '
|
||||||
'supported. Please use the dedicated nested endpoint.'))
|
'dedicated nested endpoint.'))
|
||||||
|
|
||||||
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
|
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
|
||||||
Item.clean_available(data.get('available_from'), data.get('available_until'))
|
Item.clean_available(data.get('available_from'), data.get('available_until'))
|
||||||
@@ -386,13 +347,6 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
ItemAddOn.clean_max_min_count(addon_data.get('max_count', 0), addon_data.get('min_count', 0))
|
ItemAddOn.clean_max_min_count(addon_data.get('max_count', 0), addon_data.get('min_count', 0))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def validate_program_times(self, value):
|
|
||||||
if not self.instance:
|
|
||||||
for program_time_data in value:
|
|
||||||
ItemProgramTime.clean_start_end(self, start=program_time_data.get('start', None),
|
|
||||||
end=program_time_data.get('end', None))
|
|
||||||
return value
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def item_meta_properties(self):
|
def item_meta_properties(self):
|
||||||
return {
|
return {
|
||||||
@@ -410,7 +364,6 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
variations_data = validated_data.pop('variations') if 'variations' in validated_data else {}
|
variations_data = validated_data.pop('variations') if 'variations' in validated_data else {}
|
||||||
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
|
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
|
||||||
bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {}
|
bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {}
|
||||||
program_times_data = validated_data.pop('program_times') if 'program_times' in validated_data else {}
|
|
||||||
meta_data = validated_data.pop('meta_data', None)
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
picture = validated_data.pop('picture', None)
|
picture = validated_data.pop('picture', None)
|
||||||
require_membership_types = validated_data.pop('require_membership_types', [])
|
require_membership_types = validated_data.pop('require_membership_types', [])
|
||||||
@@ -445,8 +398,6 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
ItemAddOn.objects.create(base_item=item, **addon_data)
|
ItemAddOn.objects.create(base_item=item, **addon_data)
|
||||||
for bundle_data in bundles_data:
|
for bundle_data in bundles_data:
|
||||||
ItemBundle.objects.create(base_item=item, **bundle_data)
|
ItemBundle.objects.create(base_item=item, **bundle_data)
|
||||||
for program_time_data in program_times_data:
|
|
||||||
ItemProgramTime.objects.create(item=item, **program_time_data)
|
|
||||||
|
|
||||||
# Meta data
|
# Meta data
|
||||||
if meta_data is not None:
|
if meta_data is not None:
|
||||||
@@ -599,7 +550,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
|||||||
if full_data.get('show_during_checkin') and full_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
|
if full_data.get('show_during_checkin') and full_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
|
||||||
raise ValidationError(_('This type of question cannot be shown during check-in.'))
|
raise ValidationError(_('This type of question cannot be shown during check-in.'))
|
||||||
|
|
||||||
Question.clean_items(event, full_data.get('items') or [])
|
Question.clean_items(event, full_data.get('items'))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def validate_options(self, value):
|
def validate_options(self, value):
|
||||||
@@ -615,7 +566,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
options_data = validated_data.pop('options') if 'options' in validated_data else []
|
options_data = validated_data.pop('options') if 'options' in validated_data else []
|
||||||
items = validated_data.pop('items', [])
|
items = validated_data.pop('items')
|
||||||
|
|
||||||
question = Question.objects.create(**validated_data)
|
question = Question.objects.create(**validated_data)
|
||||||
question.items.set(items)
|
question.items.set(items)
|
||||||
@@ -631,7 +582,7 @@ class QuotaSerializer(I18nAwareModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Quota
|
model = Quota
|
||||||
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out',
|
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent', 'closed', 'close_when_sold_out',
|
||||||
'release_after_exit', 'available', 'available_number', 'ignore_for_event_availability')
|
'release_after_exit', 'available', 'available_number')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -24,7 +24,7 @@ from decimal import Decimal
|
|||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.order import OrderPositionSerializer
|
from pretix.api.serializers.order import OrderPositionSerializer
|
||||||
@@ -66,9 +66,6 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if 'linked_giftcard' in self.context['request'].query_params.getlist('expand'):
|
if 'linked_giftcard' in self.context['request'].query_params.getlist('expand'):
|
||||||
if not self.context["can_read_giftcards"]:
|
|
||||||
raise PermissionDenied("No permission to access gift card details.")
|
|
||||||
|
|
||||||
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True, context=self.context)
|
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True, context=self.context)
|
||||||
if 'linked_giftcard.owner_ticket' in self.context['request'].query_params.getlist('expand'):
|
if 'linked_giftcard.owner_ticket' in self.context['request'].query_params.getlist('expand'):
|
||||||
self.fields['linked_giftcard'].fields['owner_ticket'] = NestedOrderPositionSerializer(read_only=True, context=self.context)
|
self.fields['linked_giftcard'].fields['owner_ticket'] = NestedOrderPositionSerializer(read_only=True, context=self.context)
|
||||||
@@ -80,8 +77,6 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'):
|
if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'):
|
||||||
# No additional permission check performed, documented limitation of the permission system
|
|
||||||
# Would get to complex/unusable otherwise since the permission depends on the event
|
|
||||||
self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True)
|
self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True)
|
||||||
else:
|
else:
|
||||||
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
|
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
|
||||||
@@ -91,9 +86,6 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if 'customer' in self.context['request'].query_params.getlist('expand'):
|
if 'customer' in self.context['request'].query_params.getlist('expand'):
|
||||||
if not self.context["can_read_customers"]:
|
|
||||||
raise PermissionDenied("No permission to access customer details.")
|
|
||||||
|
|
||||||
self.fields['customer'] = CustomerSerializer(read_only=True)
|
self.fields['customer'] = CustomerSerializer(read_only=True)
|
||||||
else:
|
else:
|
||||||
self.fields['customer'] = serializers.SlugRelatedField(
|
self.fields['customer'] = serializers.SlugRelatedField(
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -19,7 +19,6 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# 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/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from collections import Counter, defaultdict
|
from collections import Counter, defaultdict
|
||||||
@@ -43,7 +42,6 @@ from rest_framework.reverse import reverse
|
|||||||
|
|
||||||
from pretix.api.serializers import CompatibleJSONField
|
from pretix.api.serializers import CompatibleJSONField
|
||||||
from pretix.api.serializers.event import SubEventSerializer
|
from pretix.api.serializers.event import SubEventSerializer
|
||||||
from pretix.api.serializers.forms import form_field_to_serializer_field
|
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.item import (
|
from pretix.api.serializers.item import (
|
||||||
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
||||||
@@ -51,29 +49,23 @@ from pretix.api.serializers.item import (
|
|||||||
from pretix.api.signals import order_api_details, orderposition_api_details
|
from pretix.api.signals import order_api_details, orderposition_api_details
|
||||||
from pretix.base.decimal import round_decimal
|
from pretix.base.decimal import round_decimal
|
||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.invoicing.transmission import get_transmission_types
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedFile, Checkin, Customer, Device, GiftCard, Invoice, InvoiceAddress,
|
CachedFile, Checkin, Customer, Invoice, InvoiceAddress, InvoiceLine, Item,
|
||||||
InvoiceLine, Item, ItemVariation, Order, OrderPosition, Question,
|
ItemVariation, Order, OrderPosition, Question, QuestionAnswer,
|
||||||
QuestionAnswer, ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule,
|
ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule, Voucher,
|
||||||
Voucher,
|
|
||||||
)
|
)
|
||||||
from pretix.base.models.orders import (
|
from pretix.base.models.orders import (
|
||||||
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||||
PrintLog, RevokedTicketSecret, Transaction,
|
PrintLog, RevokedTicketSecret,
|
||||||
)
|
)
|
||||||
from pretix.base.payment import GiftCardPayment, PaymentException
|
|
||||||
from pretix.base.pdf import get_images, get_variables
|
from pretix.base.pdf import get_images, get_variables
|
||||||
from pretix.base.services.cart import error_messages
|
from pretix.base.services.cart import error_messages
|
||||||
from pretix.base.services.locking import LOCK_TRUST_WINDOW, lock_objects
|
from pretix.base.services.locking import LOCK_TRUST_WINDOW, lock_objects
|
||||||
from pretix.base.services.pricing import (
|
from pretix.base.services.pricing import (
|
||||||
apply_discounts, apply_rounding, get_line_price, get_listed_price,
|
apply_discounts, get_line_price, get_listed_price, is_included_for_free,
|
||||||
is_included_for_free,
|
|
||||||
)
|
)
|
||||||
from pretix.base.services.quotas import QuotaAvailability
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
from pretix.base.settings import (
|
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
||||||
COUNTRIES_WITH_STATE_IN_ADDRESS, ROUNDING_MODES,
|
|
||||||
)
|
|
||||||
from pretix.base.signals import register_ticket_outputs
|
from pretix.base.signals import register_ticket_outputs
|
||||||
from pretix.helpers.countries import CachedCountries
|
from pretix.helpers.countries import CachedCountries
|
||||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
@@ -110,13 +102,6 @@ class CountryField(serializers.Field):
|
|||||||
return str(src) if src else None
|
return str(src) if src else None
|
||||||
|
|
||||||
|
|
||||||
class TransmissionInfoSerializer(serializers.Serializer):
|
|
||||||
def __init__(self, *args, transmission_type, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
for k, v in transmission_type.invoice_address_form_fields.items():
|
|
||||||
self.fields[k] = form_field_to_serializer_field(v)
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
||||||
country = CompatibleCountryField(source='*')
|
country = CompatibleCountryField(source='*')
|
||||||
name = serializers.CharField(required=False)
|
name = serializers.CharField(required=False)
|
||||||
@@ -124,8 +109,7 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = InvoiceAddress
|
model = InvoiceAddress
|
||||||
fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country',
|
fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country',
|
||||||
'state', 'vat_id', 'vat_id_validated', 'custom_field', 'internal_reference', 'transmission_type',
|
'state', 'vat_id', 'vat_id_validated', 'custom_field', 'internal_reference')
|
||||||
'transmission_info')
|
|
||||||
read_only_fields = ('last_modified',)
|
read_only_fields = ('last_modified',)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -163,48 +147,6 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
|
|||||||
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
||||||
)
|
)
|
||||||
|
|
||||||
if data.get("transmission_type"):
|
|
||||||
for t in get_transmission_types():
|
|
||||||
if data.get("transmission_type") == t.identifier:
|
|
||||||
if not t.is_available(self.context["request"].event, data.get("country"), data.get("is_business")):
|
|
||||||
raise ValidationError({
|
|
||||||
"transmission_type": "The selected transmission type is not available for this country or address type."
|
|
||||||
})
|
|
||||||
|
|
||||||
ts = TransmissionInfoSerializer(transmission_type=t, data=data.get("transmission_info", {}))
|
|
||||||
try:
|
|
||||||
ts.is_valid(raise_exception=True)
|
|
||||||
except ValidationError as e:
|
|
||||||
raise ValidationError(
|
|
||||||
{"transmission_info": e.detail}
|
|
||||||
)
|
|
||||||
data["transmission_info"] = ts.validated_data
|
|
||||||
|
|
||||||
required_fields = t.invoice_address_form_fields_required(data.get("country"), data.get("is_business"))
|
|
||||||
for r in required_fields:
|
|
||||||
if r in self.fields:
|
|
||||||
if not data.get(r):
|
|
||||||
raise ValidationError(
|
|
||||||
{r: "This field is required for the selected type of invoice transmission."}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if not ts.validated_data.get(r):
|
|
||||||
raise ValidationError(
|
|
||||||
{"transmission_info": {r: "This field is required for the selected type of invoice transmission."}}
|
|
||||||
)
|
|
||||||
break # do not call else branch of for loop
|
|
||||||
elif t.is_exclusive(self.context["request"].event, data.get("country"), data.get("is_business")):
|
|
||||||
if t.is_available(self.context["request"].event, data.get("country"), data.get("is_business")):
|
|
||||||
raise ValidationError({
|
|
||||||
"transmission_type": "The transmission type '%s' must be used for this country or address type." % (
|
|
||||||
t.identifier,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
raise ValidationError(
|
|
||||||
{"transmission_type": "Unknown transmission type."}
|
|
||||||
)
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -331,18 +273,6 @@ class AnswerSerializer(I18nAwareModelSerializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class InlineCheckinSerializer(I18nAwareModelSerializer):
|
|
||||||
device_id = serializers.SlugRelatedField(
|
|
||||||
source='device',
|
|
||||||
slug_field='device_id',
|
|
||||||
read_only=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Checkin
|
|
||||||
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'device_id', 'type')
|
|
||||||
|
|
||||||
|
|
||||||
class CheckinSerializer(I18nAwareModelSerializer):
|
class CheckinSerializer(I18nAwareModelSerializer):
|
||||||
device_id = serializers.SlugRelatedField(
|
device_id = serializers.SlugRelatedField(
|
||||||
source='device',
|
source='device',
|
||||||
@@ -352,10 +282,7 @@ class CheckinSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Checkin
|
model = Checkin
|
||||||
fields = (
|
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'device_id', 'type')
|
||||||
'id', 'successful', 'error_reason', 'error_explanation', 'position', 'datetime', 'list', 'created',
|
|
||||||
'auto_checked_in', 'gate', 'device', 'device_id', 'type'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PrintLogSerializer(serializers.ModelSerializer):
|
class PrintLogSerializer(serializers.ModelSerializer):
|
||||||
@@ -581,7 +508,7 @@ class OrderPositionPluginDataField(serializers.Field):
|
|||||||
|
|
||||||
|
|
||||||
class OrderPositionSerializer(I18nAwareModelSerializer):
|
class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||||
checkins = InlineCheckinSerializer(many=True, read_only=True)
|
checkins = CheckinSerializer(many=True, read_only=True)
|
||||||
print_logs = PrintLogSerializer(many=True, read_only=True)
|
print_logs = PrintLogSerializer(many=True, read_only=True)
|
||||||
answers = AnswerSerializer(many=True)
|
answers = AnswerSerializer(many=True)
|
||||||
downloads = PositionDownloadsField(source='*', read_only=True)
|
downloads = PositionDownloadsField(source='*', read_only=True)
|
||||||
@@ -615,7 +542,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
|||||||
# /events/…/checkinlists/…/positions/
|
# /events/…/checkinlists/…/positions/
|
||||||
# We're unable to check this on this level if we're on /checkinrpc/, in which case we rely on the view
|
# We're unable to check this on this level if we're on /checkinrpc/, in which case we rely on the view
|
||||||
# layer to not set pdf_data=true in the first place.
|
# layer to not set pdf_data=true in the first place.
|
||||||
request and hasattr(request, 'eventpermset') and 'event.orders:read' not in request.eventpermset
|
request and hasattr(request, 'eventpermset') and 'can_view_orders' not in request.eventpermset
|
||||||
)
|
)
|
||||||
if ('pdf_data' in self.context and not self.context['pdf_data']) or pdf_data_forbidden:
|
if ('pdf_data' in self.context and not self.context['pdf_data']) or pdf_data_forbidden:
|
||||||
self.fields.pop('pdf_data', None)
|
self.fields.pop('pdf_data', None)
|
||||||
@@ -638,14 +565,6 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
|||||||
return entry
|
return entry
|
||||||
|
|
||||||
|
|
||||||
class OrganizerOrderPositionSerializer(OrderPositionSerializer):
|
|
||||||
event = SlugRelatedField(slug_field='slug', read_only=True)
|
|
||||||
|
|
||||||
class Meta(OrderPositionSerializer.Meta):
|
|
||||||
fields = OrderPositionSerializer.Meta.fields + ('event',)
|
|
||||||
read_only_fields = OrderPositionSerializer.Meta.read_only_fields + ('event',)
|
|
||||||
|
|
||||||
|
|
||||||
class RequireAttentionField(serializers.Field):
|
class RequireAttentionField(serializers.Field):
|
||||||
def to_representation(self, instance: OrderPosition):
|
def to_representation(self, instance: OrderPosition):
|
||||||
return instance.require_checkin_attention
|
return instance.require_checkin_attention
|
||||||
@@ -714,16 +633,6 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
|||||||
if 'answers.question' in self.context['expand']:
|
if 'answers.question' in self.context['expand']:
|
||||||
self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True)
|
self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True)
|
||||||
|
|
||||||
if 'addons' in self.context['expand']:
|
|
||||||
# Experimental feature, undocumented on purpose for now in case we need to remove it again
|
|
||||||
# for performance reasons
|
|
||||||
subl = CheckinListOrderPositionSerializer(read_only=True, many=True, context={
|
|
||||||
**self.context,
|
|
||||||
'expand': [v for v in self.context['expand'] if v != 'addons'],
|
|
||||||
'pdf_data': False,
|
|
||||||
})
|
|
||||||
self.fields['addons'] = subl
|
|
||||||
|
|
||||||
|
|
||||||
class OrderPaymentTypeField(serializers.Field):
|
class OrderPaymentTypeField(serializers.Field):
|
||||||
# TODO: Remove after pretix 2.2
|
# TODO: Remove after pretix 2.2
|
||||||
@@ -769,11 +678,7 @@ class PaymentDetailsField(serializers.Field):
|
|||||||
pp = value.payment_provider
|
pp = value.payment_provider
|
||||||
if not pp:
|
if not pp:
|
||||||
return {}
|
return {}
|
||||||
try:
|
return pp.api_payment_details(value)
|
||||||
return pp.api_payment_details(value)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to retrieve payment_details")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
class OrderPaymentSerializer(I18nAwareModelSerializer):
|
class OrderPaymentSerializer(I18nAwareModelSerializer):
|
||||||
@@ -876,15 +781,14 @@ class OrderSerializer(I18nAwareModelSerializer):
|
|||||||
list_serializer_class = OrderListSerializer
|
list_serializer_class = OrderListSerializer
|
||||||
fields = (
|
fields = (
|
||||||
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
||||||
'payment_provider', 'fees', 'total', 'tax_rounding_mode', 'comment', 'custom_followup_at', 'invoice_address',
|
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
||||||
'positions', 'downloads', 'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds',
|
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
||||||
'require_approval', 'sales_channel', 'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date',
|
'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date', 'plugin_data',
|
||||||
'plugin_data',
|
|
||||||
)
|
)
|
||||||
read_only_fields = (
|
read_only_fields = (
|
||||||
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
||||||
'payment_provider', 'fees', 'total', 'tax_rounding_mode', 'positions', 'downloads', 'customer',
|
'payment_provider', 'fees', 'total', 'positions', 'downloads', 'customer',
|
||||||
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel', 'cancellation_date',
|
'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel', 'cancellation_date'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -1049,7 +953,7 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
|
||||||
'company', 'street', 'zipcode', 'city', 'country', 'state', 'is_bundled',
|
'company', 'street', 'zipcode', 'city', 'country', 'state', 'is_bundled',
|
||||||
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher', 'valid_from', 'valid_until',
|
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher', 'valid_from', 'valid_until',
|
||||||
'requested_valid_from', 'use_reusable_medium', 'discount')
|
'requested_valid_from', 'use_reusable_medium')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -1145,10 +1049,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
|
||||||
)
|
)
|
||||||
|
|
||||||
if data.get('price') is None and data.get('discount'):
|
|
||||||
raise ValidationError(
|
|
||||||
{'discount': ['You can only specify a discount if you do the price computation, but price is not set.']}
|
|
||||||
)
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -1203,14 +1103,11 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
queryset=SalesChannel.objects.none(),
|
queryset=SalesChannel.objects.none(),
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
tax_rounding_mode = serializers.ChoiceField(choices=ROUNDING_MODES, allow_null=True, required=False,)
|
|
||||||
locale = serializers.ChoiceField(choices=[], required=False, allow_null=True)
|
locale = serializers.ChoiceField(choices=[], required=False, allow_null=True)
|
||||||
use_gift_cards = serializers.ListField(child=serializers.CharField(required=False), required=False)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
|
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
|
||||||
self.fields['positions'].child.fields['discount'].queryset = self.context['event'].discounts.all()
|
|
||||||
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
|
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
|
||||||
self.fields['expires'].required = False
|
self.fields['expires'].required = False
|
||||||
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||||
@@ -1221,7 +1118,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||||
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
|
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
|
||||||
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
|
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
|
||||||
'require_approval', 'valid_if_pending', 'expires', 'api_meta', 'tax_rounding_mode', 'use_gift_cards')
|
'require_approval', 'valid_if_pending', 'expires', 'api_meta')
|
||||||
|
|
||||||
def validate_payment_provider(self, pp):
|
def validate_payment_provider(self, pp):
|
||||||
if pp is None:
|
if pp is None:
|
||||||
@@ -1230,18 +1127,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
raise ValidationError('The given payment provider is not known.')
|
raise ValidationError('The given payment provider is not known.')
|
||||||
return pp
|
return pp
|
||||||
|
|
||||||
def validate_payment_info(self, info):
|
|
||||||
if info:
|
|
||||||
try:
|
|
||||||
obj = json.loads(info)
|
|
||||||
except ValueError:
|
|
||||||
raise ValidationError('payment_info must be valid JSON.')
|
|
||||||
|
|
||||||
if not isinstance(obj, dict):
|
|
||||||
# only objects are allowed
|
|
||||||
raise ValidationError('payment_info must be a JSON object.')
|
|
||||||
return info
|
|
||||||
|
|
||||||
def validate_expires(self, expires):
|
def validate_expires(self, expires):
|
||||||
if expires < now():
|
if expires < now():
|
||||||
raise ValidationError('Expiration date must be in the future.')
|
raise ValidationError('Expiration date must be in the future.')
|
||||||
@@ -1316,14 +1201,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
payment_date = validated_data.pop('payment_date', now())
|
payment_date = validated_data.pop('payment_date', now())
|
||||||
force = validated_data.pop('force', False)
|
force = validated_data.pop('force', False)
|
||||||
simulate = validated_data.pop('simulate', False)
|
simulate = validated_data.pop('simulate', False)
|
||||||
gift_card_secrets = validated_data.pop('use_gift_cards') if 'use_gift_cards' in validated_data else []
|
|
||||||
|
|
||||||
if (payment_provider is not None or payment_info != '{}') and len(gift_card_secrets) > 0:
|
|
||||||
raise ValidationError({"use_gift_cards": ['The attribute use_gift_cards is not compatible with payment_provider or payment_info']})
|
|
||||||
if validated_data.get('status') != Order.STATUS_PENDING and len(gift_card_secrets) > 0:
|
|
||||||
raise ValidationError({"use_gift_cards": ['The attribute use_gift_cards is only supported for orders that are created as pending']})
|
|
||||||
if len(set(gift_card_secrets)) != len(gift_card_secrets):
|
|
||||||
raise ValidationError({"use_gift_cards": ['Multiple copies of the same gift card secret are not allowed']})
|
|
||||||
|
|
||||||
if not validated_data.get("sales_channel"):
|
if not validated_data.get("sales_channel"):
|
||||||
validated_data["sales_channel"] = self.context['event'].organizer.sales_channels.get(identifier="web")
|
validated_data["sales_channel"] = self.context['event'].organizer.sales_channels.get(identifier="web")
|
||||||
@@ -1638,22 +1515,19 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
pos.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
|
pos.voucher_budget_use = max(listed_price - price_after_voucher, Decimal('0.00'))
|
||||||
|
|
||||||
order_positions = [pos_data['__instance'] for pos_data in positions_data]
|
order_positions = [pos_data['__instance'] for pos_data in positions_data]
|
||||||
if not any([p.get("discount") for p in positions_data]):
|
discount_results = apply_discounts(
|
||||||
# If any discount is set by the client (i.e. pretixPOS), we do not recalculate but believe the client
|
self.context['event'],
|
||||||
# to avoid differences in end results.
|
order.sales_channel,
|
||||||
discount_results = apply_discounts(
|
[
|
||||||
self.context['event'],
|
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.price,
|
||||||
order.sales_channel,
|
bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
|
||||||
[
|
for cp in order_positions
|
||||||
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.price,
|
]
|
||||||
cp.addon_to, cp.is_bundled, pos._voucher_discount)
|
)
|
||||||
for cp in order_positions
|
for cp, (new_price, discount) in zip(order_positions, discount_results):
|
||||||
]
|
if new_price != pos.price and pos._auto_generated_price:
|
||||||
)
|
pos.price = new_price
|
||||||
for cp, (new_price, discount) in zip(order_positions, discount_results):
|
pos.discount = discount
|
||||||
if new_price != pos.price and pos._auto_generated_price:
|
|
||||||
pos.price = new_price
|
|
||||||
pos.discount = discount
|
|
||||||
|
|
||||||
# Save instances
|
# Save instances
|
||||||
for pos_data in positions_data:
|
for pos_data in positions_data:
|
||||||
@@ -1767,32 +1641,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
else:
|
else:
|
||||||
f.save()
|
f.save()
|
||||||
|
|
||||||
rounding_mode = validated_data.get("tax_rounding_mode")
|
order.total += sum([f.value for f in fees])
|
||||||
if not rounding_mode:
|
|
||||||
if isinstance(self.context.get("auth"), Device):
|
|
||||||
# Safety fallback to avoid differences in tax reporting
|
|
||||||
brand = self.context.get("auth").software_brand or ""
|
|
||||||
if "pretixPOS" in brand or "pretixKIOSK" in brand:
|
|
||||||
rounding_mode = "line"
|
|
||||||
if not rounding_mode:
|
|
||||||
rounding_mode = self.context["event"].settings.tax_rounding
|
|
||||||
changed = apply_rounding(
|
|
||||||
rounding_mode,
|
|
||||||
ia,
|
|
||||||
self.context["event"].currency,
|
|
||||||
[*pos_map.values(), *fees]
|
|
||||||
)
|
|
||||||
for line in changed:
|
|
||||||
if isinstance(line, OrderPosition):
|
|
||||||
line.save(update_fields=[
|
|
||||||
"price", "price_includes_rounding_correction", "tax_value", "tax_value_includes_rounding_correction"
|
|
||||||
])
|
|
||||||
elif isinstance(line, OrderFee):
|
|
||||||
line.save(update_fields=[
|
|
||||||
"value", "value_includes_rounding_correction", "tax_value", "tax_value_includes_rounding_correction"
|
|
||||||
])
|
|
||||||
|
|
||||||
order.total = sum([c.price for c in pos_map.values()]) + sum([f.value for f in fees])
|
|
||||||
if simulate:
|
if simulate:
|
||||||
order.fees = fees
|
order.fees = fees
|
||||||
order.positions = pos_map.values()
|
order.positions = pos_map.values()
|
||||||
@@ -1808,45 +1657,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
if order.total != Decimal('0.00') and order.event.currency == "XXX":
|
if order.total != Decimal('0.00') and order.event.currency == "XXX":
|
||||||
raise ValidationError('Paid products not supported without a valid currency.')
|
raise ValidationError('Paid products not supported without a valid currency.')
|
||||||
|
|
||||||
for gift_card_secret in gift_card_secrets:
|
|
||||||
try:
|
|
||||||
if order.status != Order.STATUS_PAID:
|
|
||||||
gift_card_payment_provider = GiftCardPayment(event=order.event)
|
|
||||||
|
|
||||||
gc = order.event.organizer.accepted_gift_cards.get(
|
|
||||||
secret=gift_card_secret
|
|
||||||
)
|
|
||||||
|
|
||||||
payment = order.payments.create(
|
|
||||||
amount=min(order.pending_sum, gc.value),
|
|
||||||
provider=gift_card_payment_provider.identifier,
|
|
||||||
info_data={
|
|
||||||
'gift_card': gc.pk,
|
|
||||||
'gift_card_secret': gc.secret,
|
|
||||||
'retry': True
|
|
||||||
},
|
|
||||||
state=OrderPayment.PAYMENT_STATE_CREATED
|
|
||||||
)
|
|
||||||
gift_card_payment_provider.execute_payment(request=None, payment=payment, is_early_special_case=True)
|
|
||||||
|
|
||||||
if order.pending_sum <= Decimal('0.00'):
|
|
||||||
order.status = Order.STATUS_PAID
|
|
||||||
|
|
||||||
except PaymentException:
|
|
||||||
pass
|
|
||||||
|
|
||||||
except GiftCard.DoesNotExist as e:
|
|
||||||
payment = order.payments.create(
|
|
||||||
amount=order.pending_sum,
|
|
||||||
provider=GiftCardPayment.identifier,
|
|
||||||
info_data={
|
|
||||||
'gift_card_secret': gift_card_secret,
|
|
||||||
},
|
|
||||||
state=OrderPayment.PAYMENT_STATE_CREATED
|
|
||||||
)
|
|
||||||
payment.fail(info={**payment.info_data, 'error': str(e)},
|
|
||||||
send_mail=False)
|
|
||||||
|
|
||||||
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID and not validated_data.get('require_approval'):
|
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID and not validated_data.get('require_approval'):
|
||||||
order.status = Order.STATUS_PAID
|
order.status = Order.STATUS_PAID
|
||||||
order.save()
|
order.save()
|
||||||
@@ -1895,14 +1705,12 @@ class LinePositionField(serializers.IntegerField):
|
|||||||
|
|
||||||
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
||||||
position = LinePositionField(read_only=True)
|
position = LinePositionField(read_only=True)
|
||||||
event_date_from = serializers.DateTimeField(read_only=True, source="period_start")
|
|
||||||
event_date_to = serializers.DateTimeField(read_only=True, source="period_end")
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InvoiceLine
|
model = InvoiceLine
|
||||||
fields = ('position', 'description', 'item', 'variation', 'subevent', 'attendee_name', 'event_date_from',
|
fields = ('position', 'description', 'item', 'variation', 'subevent', 'attendee_name', 'event_date_from',
|
||||||
'event_date_to', 'period_start', 'period_end', 'gross_value', 'tax_value', 'tax_rate', 'tax_code',
|
'event_date_to', 'gross_value', 'tax_value', 'tax_rate', 'tax_code', 'tax_name', 'fee_type',
|
||||||
'tax_name', 'fee_type', 'fee_internal_type', 'event_location')
|
'fee_internal_type', 'event_location')
|
||||||
|
|
||||||
|
|
||||||
class InvoiceSerializer(I18nAwareModelSerializer):
|
class InvoiceSerializer(I18nAwareModelSerializer):
|
||||||
@@ -1916,14 +1724,13 @@ class InvoiceSerializer(I18nAwareModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Invoice
|
model = Invoice
|
||||||
fields = ('event', 'order', 'number', 'is_cancellation', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
|
fields = ('event', 'order', 'number', 'is_cancellation', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
|
||||||
'invoice_from_city', 'invoice_from_state', 'invoice_from_country', 'invoice_from_tax_id', 'invoice_from_vat_id',
|
'invoice_from_city', 'invoice_from_country', 'invoice_from_tax_id', 'invoice_from_vat_id',
|
||||||
'invoice_to', 'invoice_to_is_business', 'invoice_to_company', 'invoice_to_name', 'invoice_to_street',
|
'invoice_to', 'invoice_to_company', 'invoice_to_name', 'invoice_to_street', 'invoice_to_zipcode',
|
||||||
'invoice_to_zipcode', 'invoice_to_city', 'invoice_to_state', 'invoice_to_country', 'invoice_to_vat_id',
|
'invoice_to_city', 'invoice_to_state', 'invoice_to_country', 'invoice_to_vat_id', 'invoice_to_beneficiary',
|
||||||
'invoice_to_beneficiary', 'invoice_to_transmission_info', 'custom_field', 'date', 'refers', 'locale',
|
'custom_field', 'date', 'refers', 'locale',
|
||||||
'introductory_text', 'additional_text', 'payment_provider_text', 'payment_provider_stamp',
|
'introductory_text', 'additional_text', 'payment_provider_text', 'payment_provider_stamp',
|
||||||
'footer_text', 'lines', 'foreign_currency_display', 'foreign_currency_rate',
|
'footer_text', 'lines', 'foreign_currency_display', 'foreign_currency_rate',
|
||||||
'foreign_currency_rate_date', 'internal_reference', 'transmission_type', 'transmission_provider',
|
'foreign_currency_rate_date', 'internal_reference')
|
||||||
'transmission_status', 'transmission_date')
|
|
||||||
|
|
||||||
|
|
||||||
class OrderPaymentCreateSerializer(I18nAwareModelSerializer):
|
class OrderPaymentCreateSerializer(I18nAwareModelSerializer):
|
||||||
@@ -1976,23 +1783,3 @@ class BlockedTicketSecretSerializer(I18nAwareModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = BlockedTicketSecret
|
model = BlockedTicketSecret
|
||||||
fields = ('id', 'secret', 'updated', 'blocked')
|
fields = ('id', 'secret', 'updated', 'blocked')
|
||||||
|
|
||||||
|
|
||||||
class TransactionSerializer(I18nAwareModelSerializer):
|
|
||||||
order = serializers.SlugRelatedField(slug_field="code", read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Transaction
|
|
||||||
fields = (
|
|
||||||
"id", "order", "created", "datetime", "positionid", "count", "item", "variation",
|
|
||||||
"subevent", "price", "tax_rate", "tax_rule", "tax_code", "tax_value", "fee_type",
|
|
||||||
"internal_type"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizerTransactionSerializer(TransactionSerializer):
|
|
||||||
event = serializers.SlugRelatedField(source="order.event", slug_field="slug", read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Transaction
|
|
||||||
fields = TransactionSerializer.Meta.fields + ("event",)
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -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 OrderChangeManager, OrderError
|
from pretix.base.services.orders import 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,10 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
ocm: OrderChangeManager = self.context['ocm']
|
ocm = self.context['ocm']
|
||||||
check_quotas = self.context.get('check_quotas', True)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
new_position = ocm.add_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'),
|
||||||
@@ -97,8 +96,8 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
|
|||||||
valid_until=validated_data.get('valid_until'),
|
valid_until=validated_data.get('valid_until'),
|
||||||
)
|
)
|
||||||
if self.context.get('commit', True):
|
if self.context.get('commit', True):
|
||||||
ocm.commit(check_quotas=check_quotas)
|
ocm.commit()
|
||||||
return new_position.position
|
return validated_data['order'].positions.order_by('-positionid').first()
|
||||||
else:
|
else:
|
||||||
return OrderPosition() # fake to appease DRF
|
return OrderPosition() # fake to appease DRF
|
||||||
except OrderError as e:
|
except OrderError as e:
|
||||||
@@ -131,7 +130,7 @@ class OrderFeeCreateForExistingOrderSerializer(OrderFeeCreateSerializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
ocm: OrderChangeManager = self.context['ocm']
|
ocm = self.context['ocm']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
f = OrderFee(
|
f = OrderFee(
|
||||||
@@ -146,7 +145,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 f
|
return validated_data['order'].fees.order_by('-pk').first()
|
||||||
else:
|
else:
|
||||||
return OrderFee() # fake to appease DRF
|
return OrderFee() # fake to appease DRF
|
||||||
except OrderError as e:
|
except OrderError as e:
|
||||||
@@ -310,8 +309,7 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
ocm: OrderChangeManager = self.context['ocm']
|
ocm = self.context['ocm']
|
||||||
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)
|
||||||
variation = validated_data.get('variation', instance.variation)
|
variation = validated_data.get('variation', instance.variation)
|
||||||
@@ -358,7 +356,7 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer):
|
|||||||
ocm.change_ticket_secret(instance, secret)
|
ocm.change_ticket_secret(instance, secret)
|
||||||
|
|
||||||
if self.context.get('commit', True):
|
if self.context.get('commit', True):
|
||||||
ocm.commit(check_quotas=check_quotas)
|
ocm.commit()
|
||||||
instance.refresh_from_db()
|
instance.refresh_from_db()
|
||||||
except OrderError as e:
|
except OrderError as e:
|
||||||
raise ValidationError(str(e))
|
raise ValidationError(str(e))
|
||||||
@@ -399,7 +397,7 @@ class OrderFeeChangeSerializer(serializers.ModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
ocm: OrderChangeManager = self.context['ocm']
|
ocm = self.context['ocm']
|
||||||
value = validated_data.get('value', instance.value)
|
value = validated_data.get('value', instance.value)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -24,7 +24,6 @@ from decimal import Decimal
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db import transaction
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -33,7 +32,6 @@ from rest_framework.exceptions import ValidationError
|
|||||||
|
|
||||||
from pretix.api.auth.devicesecurity import get_all_security_profiles
|
from pretix.api.auth.devicesecurity import get_all_security_profiles
|
||||||
from pretix.api.serializers import AsymmetricField
|
from pretix.api.serializers import AsymmetricField
|
||||||
from pretix.api.serializers.fields import PluginsField
|
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.order import CompatibleJSONField
|
from pretix.api.serializers.order import CompatibleJSONField
|
||||||
from pretix.api.serializers.settings import SettingsSerializer
|
from pretix.api.serializers.settings import SettingsSerializer
|
||||||
@@ -45,19 +43,8 @@ from pretix.base.models import (
|
|||||||
SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||||
)
|
)
|
||||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||||
from pretix.base.permissions import (
|
from pretix.base.services.mail import SendMailException, mail
|
||||||
get_all_event_permission_groups, get_all_organizer_permission_groups,
|
|
||||||
)
|
|
||||||
from pretix.base.plugins import (
|
|
||||||
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
|
||||||
PLUGIN_LEVEL_ORGANIZER,
|
|
||||||
)
|
|
||||||
from pretix.base.services.mail import mail
|
|
||||||
from pretix.base.settings import validate_organizer_settings
|
from pretix.base.settings import validate_organizer_settings
|
||||||
from pretix.helpers.permission_migration import (
|
|
||||||
OLD_TO_NEW_EVENT_COMPAT, OLD_TO_NEW_EVENT_MIGRATION,
|
|
||||||
OLD_TO_NEW_ORGANIZER_COMPAT, OLD_TO_NEW_ORGANIZER_MIGRATION,
|
|
||||||
)
|
|
||||||
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
||||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
|
|
||||||
@@ -66,47 +53,13 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class OrganizerSerializer(I18nAwareModelSerializer):
|
class OrganizerSerializer(I18nAwareModelSerializer):
|
||||||
public_url = serializers.SerializerMethodField('get_organizer_url', read_only=True)
|
public_url = serializers.SerializerMethodField('get_organizer_url', read_only=True)
|
||||||
plugins = PluginsField(required=False, source='*')
|
|
||||||
name = serializers.CharField(read_only=True)
|
|
||||||
slug = serializers.CharField(read_only=True)
|
|
||||||
|
|
||||||
def get_organizer_url(self, organizer):
|
def get_organizer_url(self, organizer):
|
||||||
return build_absolute_uri(organizer, 'presale:organizer.index')
|
return build_absolute_uri(organizer, 'presale:organizer.index')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Organizer
|
model = Organizer
|
||||||
fields = ('name', 'slug', 'public_url', 'plugins')
|
fields = ('name', 'slug', 'public_url')
|
||||||
|
|
||||||
def validate_plugins(self, value):
|
|
||||||
from pretix.base.plugins import get_all_plugins
|
|
||||||
|
|
||||||
plugins_available = {
|
|
||||||
p.module: p for p in get_all_plugins(organizer=self.instance)
|
|
||||||
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
|
||||||
}
|
|
||||||
settings_holder = self.instance
|
|
||||||
|
|
||||||
allowed_levels = (PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID)
|
|
||||||
for plugin in value.get('plugins'):
|
|
||||||
if plugin not in plugins_available:
|
|
||||||
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
|
|
||||||
if getattr(plugins_available[plugin], 'restricted', False):
|
|
||||||
if plugin not in settings_holder.settings.allowed_restricted_plugins:
|
|
||||||
raise ValidationError(_('Restricted plugin: \'{name}\'.').format(name=plugin))
|
|
||||||
if getattr(plugins_available[plugin], 'level', PLUGIN_LEVEL_EVENT) not in allowed_levels:
|
|
||||||
raise ValidationError('Plugin cannot be enabled on this level: \'{name}\'.'.format(name=plugin))
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
plugins = validated_data.pop('plugins', None)
|
|
||||||
organizer = super().update(instance, validated_data)
|
|
||||||
# Plugins
|
|
||||||
if plugins is not None:
|
|
||||||
organizer.set_active_plugins(plugins)
|
|
||||||
organizer.save()
|
|
||||||
return organizer
|
|
||||||
|
|
||||||
|
|
||||||
class SeatingPlanSerializer(I18nAwareModelSerializer):
|
class SeatingPlanSerializer(I18nAwareModelSerializer):
|
||||||
@@ -313,128 +266,23 @@ class EventSlugField(serializers.SlugRelatedField):
|
|||||||
return self.context['organizer'].events.all()
|
return self.context['organizer'].events.all()
|
||||||
|
|
||||||
|
|
||||||
class PermissionMultipleChoiceField(serializers.MultipleChoiceField):
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
return {
|
|
||||||
p: True for p in super().to_internal_value(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
def to_representation(self, value):
|
|
||||||
return [p for p, v in value.items() if v]
|
|
||||||
|
|
||||||
|
|
||||||
class TeamSerializer(serializers.ModelSerializer):
|
class TeamSerializer(serializers.ModelSerializer):
|
||||||
limit_events = EventSlugField(slug_field='slug', many=True)
|
limit_events = EventSlugField(slug_field='slug', many=True)
|
||||||
limit_event_permissions = PermissionMultipleChoiceField(choices=[], required=False, allow_null=False, allow_empty=True)
|
|
||||||
limit_organizer_permissions = PermissionMultipleChoiceField(choices=[], required=False, allow_null=False, allow_empty=True)
|
|
||||||
|
|
||||||
# Legacy fields, handled in to_representation and validate
|
|
||||||
can_change_event_settings = serializers.BooleanField(required=False, write_only=True)
|
|
||||||
can_change_items = serializers.BooleanField(required=False, write_only=True)
|
|
||||||
can_view_orders = serializers.BooleanField(required=False, write_only=True)
|
|
||||||
can_change_orders = serializers.BooleanField(required=False, write_only=True)
|
|
||||||
can_checkin_orders = serializers.BooleanField(required=False, write_only=True)
|
|
||||||
can_view_vouchers = serializers.BooleanField(required=False, write_only=True)
|
|
||||||
can_change_vouchers = serializers.BooleanField(required=False, write_only=True)
|
|
||||||
can_create_events = serializers.BooleanField(required=False, write_only=True)
|
|
||||||
can_change_organizer_settings = serializers.BooleanField(required=False, write_only=True)
|
|
||||||
can_change_teams = serializers.BooleanField(required=False, write_only=True)
|
|
||||||
can_manage_gift_cards = serializers.BooleanField(required=False, write_only=True)
|
|
||||||
can_manage_customers = serializers.BooleanField(required=False, write_only=True)
|
|
||||||
can_manage_reusable_media = serializers.BooleanField(required=False, write_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Team
|
model = Team
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'name', 'require_2fa', 'all_events', 'limit_events', 'all_event_permissions', 'limit_event_permissions',
|
'id', 'name', 'require_2fa', 'all_events', 'limit_events', 'can_create_events', 'can_change_teams',
|
||||||
'all_organizer_permissions', 'limit_organizer_permissions', 'can_change_event_settings',
|
'can_change_organizer_settings', 'can_manage_gift_cards', 'can_change_event_settings',
|
||||||
'can_change_items', 'can_view_orders', 'can_change_orders', 'can_checkin_orders', 'can_view_vouchers',
|
'can_change_items', 'can_view_orders', 'can_change_orders', 'can_view_vouchers',
|
||||||
'can_change_vouchers', 'can_create_events', 'can_change_organizer_settings', 'can_change_teams',
|
'can_change_vouchers', 'can_checkin_orders', 'can_manage_customers', 'can_manage_reusable_media'
|
||||||
'can_manage_gift_cards', 'can_manage_customers', 'can_manage_reusable_media'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
event_perms_flattened = []
|
|
||||||
organizer_perms_flattened = []
|
|
||||||
for pg in get_all_event_permission_groups().values():
|
|
||||||
for action in pg.actions:
|
|
||||||
event_perms_flattened.append(f"{pg.name}:{action}")
|
|
||||||
for pg in get_all_organizer_permission_groups().values():
|
|
||||||
for action in pg.actions:
|
|
||||||
organizer_perms_flattened.append(f"{pg.name}:{action}")
|
|
||||||
|
|
||||||
self.fields['limit_event_permissions'].choices = [(p, p) for p in event_perms_flattened]
|
|
||||||
self.fields['limit_organizer_permissions'].choices = [(p, p) for p in organizer_perms_flattened]
|
|
||||||
|
|
||||||
def to_representation(self, instance):
|
|
||||||
r = super().to_representation(instance)
|
|
||||||
for old, new in OLD_TO_NEW_EVENT_COMPAT.items():
|
|
||||||
r[old] = instance.all_event_permissions or all(instance.limit_event_permissions.get(n) for n in new)
|
|
||||||
for old, new in OLD_TO_NEW_ORGANIZER_COMPAT.items():
|
|
||||||
r[old] = instance.all_organizer_permissions or all(instance.limit_organizer_permissions.get(n) for n in new)
|
|
||||||
return r
|
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
old_data_set = any(k.startswith("can_") for k in data)
|
|
||||||
new_data_set = any(k in data for k in [
|
|
||||||
"all_event_permissions", "limit_event_permissions", "all_organizer_permissions", "limit_organizer_permissions"
|
|
||||||
])
|
|
||||||
if old_data_set and new_data_set:
|
|
||||||
raise ValidationError("You cannot set deprecated and current permission attributes at the same time.")
|
|
||||||
|
|
||||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||||
full_data.update(data)
|
full_data.update(data)
|
||||||
|
|
||||||
if new_data_set:
|
|
||||||
if full_data.get('limit_event_permissions') and full_data.get('all_event_permissions'):
|
|
||||||
raise ValidationError('Do not set both limit_event_permissions and all_event_permissions.')
|
|
||||||
if full_data.get('limit_organizer_permissions') and full_data.get('all_organizer_permissions'):
|
|
||||||
raise ValidationError('Do not set both limit_organizer_permissions and all_organizer_permissions.')
|
|
||||||
|
|
||||||
if old_data_set:
|
|
||||||
# Migrate with same logic as in migration 0297_pluggable_permissions
|
|
||||||
if all(full_data.get(k) is True for k in OLD_TO_NEW_EVENT_MIGRATION.keys() if k != "can_checkin_orders"):
|
|
||||||
data["all_event_permissions"] = True
|
|
||||||
data["limit_event_permissions"] = {}
|
|
||||||
else:
|
|
||||||
data["all_event_permissions"] = False
|
|
||||||
data["limit_event_permissions"] = {}
|
|
||||||
for k, v in OLD_TO_NEW_EVENT_MIGRATION.items():
|
|
||||||
if full_data.get(k) is True:
|
|
||||||
data["limit_event_permissions"].update({kk: True for kk in v})
|
|
||||||
if all(full_data.get(k) is True for k in OLD_TO_NEW_ORGANIZER_MIGRATION.keys() if k != "can_checkin_orders"):
|
|
||||||
data["all_organizer_permissions"] = True
|
|
||||||
data["limit_organizer_permissions"] = {}
|
|
||||||
else:
|
|
||||||
data["all_organizer_permissions"] = False
|
|
||||||
data["limit_organizer_permissions"] = {}
|
|
||||||
for k, v in OLD_TO_NEW_ORGANIZER_MIGRATION.items():
|
|
||||||
if full_data.get(k) is True:
|
|
||||||
data["limit_organizer_permissions"].update({kk: True for kk in v})
|
|
||||||
|
|
||||||
if full_data.get('limit_events') and full_data.get('all_events'):
|
if full_data.get('limit_events') and full_data.get('all_events'):
|
||||||
raise ValidationError('Do not set both limit_events and all_events.')
|
raise ValidationError('Do not set both limit_events and all_events.')
|
||||||
|
|
||||||
full_data.update(data)
|
|
||||||
for pg in get_all_event_permission_groups().values():
|
|
||||||
requested = ",".join(sorted(
|
|
||||||
a for a in pg.actions if self.instance and full_data["limit_event_permissions"].get(f"{pg.name}:{a}")
|
|
||||||
))
|
|
||||||
if requested not in (",".join(sorted(opt.actions)) for opt in pg.options):
|
|
||||||
possible = '\' or \''.join(','.join(opt.actions) for opt in pg.options)
|
|
||||||
raise ValidationError(f"For permission group {pg.name}, the valid combinations of actions are "
|
|
||||||
f"'{possible}' but you tried to set '{requested}'.")
|
|
||||||
for pg in get_all_organizer_permission_groups().values():
|
|
||||||
requested = ",".join(sorted(
|
|
||||||
a for a in pg.actions if self.instance and full_data["limit_organizer_permissions"].get(f"{pg.name}:{a}")
|
|
||||||
))
|
|
||||||
if requested not in (",".join(sorted(opt.actions)) for opt in pg.options):
|
|
||||||
possible = '\' or \''.join(','.join(opt.actions) for opt in pg.options)
|
|
||||||
raise ValidationError(f"For permission group {pg.name}, the valid combinations of actions are "
|
|
||||||
f"'{possible}' but you tried to set '{requested}'.")
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -451,7 +299,7 @@ class DeviceSerializer(serializers.ModelSerializer):
|
|||||||
created = serializers.DateTimeField(read_only=True)
|
created = serializers.DateTimeField(read_only=True)
|
||||||
revoked = serializers.BooleanField(read_only=True)
|
revoked = serializers.BooleanField(read_only=True)
|
||||||
initialized = serializers.DateTimeField(read_only=True)
|
initialized = serializers.DateTimeField(read_only=True)
|
||||||
initialization_token = serializers.CharField(read_only=True)
|
initialization_token = serializers.DateTimeField(read_only=True)
|
||||||
security_profile = serializers.ChoiceField(choices=[], required=False, default="full")
|
security_profile = serializers.ChoiceField(choices=[], required=False, default="full")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -465,8 +313,6 @@ class DeviceSerializer(serializers.ModelSerializer):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['security_profile'].choices = [(k, v.verbose_name) for k, v in get_all_security_profiles().items()]
|
self.fields['security_profile'].choices = [(k, v.verbose_name) for k, v in get_all_security_profiles().items()]
|
||||||
if not self.context['can_see_tokens']:
|
|
||||||
del self.fields['initialization_token']
|
|
||||||
|
|
||||||
|
|
||||||
class TeamInviteSerializer(serializers.ModelSerializer):
|
class TeamInviteSerializer(serializers.ModelSerializer):
|
||||||
@@ -477,22 +323,24 @@ class TeamInviteSerializer(serializers.ModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _send_invite(self, instance):
|
def _send_invite(self, instance):
|
||||||
mail(
|
try:
|
||||||
instance.email,
|
mail(
|
||||||
_('Account invitation'),
|
instance.email,
|
||||||
'pretixcontrol/email/invitation.txt',
|
_('pretix account invitation'),
|
||||||
{
|
'pretixcontrol/email/invitation.txt',
|
||||||
'instance': settings.PRETIX_INSTANCE_NAME,
|
{
|
||||||
'user': self,
|
'user': self,
|
||||||
'organizer': self.context['organizer'].name,
|
'organizer': self.context['organizer'].name,
|
||||||
'team': instance.team.name,
|
'team': instance.team.name,
|
||||||
'url': build_global_uri('control:auth.invite', kwargs={
|
'url': build_global_uri('control:auth.invite', kwargs={
|
||||||
'token': instance.token
|
'token': instance.token
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
event=None,
|
event=None,
|
||||||
locale=get_language_without_region() # TODO: expose?
|
locale=get_language_without_region() # TODO: expose?
|
||||||
)
|
)
|
||||||
|
except SendMailException:
|
||||||
|
pass # Already logged
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
if 'email' in validated_data:
|
if 'email' in validated_data:
|
||||||
@@ -551,14 +399,10 @@ class TeamMemberSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class OrganizerSettingsSerializer(SettingsSerializer):
|
class OrganizerSettingsSerializer(SettingsSerializer):
|
||||||
default_write_permission = 'organizer.settings.general:write'
|
|
||||||
default_fields = [
|
default_fields = [
|
||||||
# These are readable for all users with access to the events, therefore secrets stored in the settings store
|
|
||||||
# should not be included!
|
|
||||||
'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',
|
||||||
@@ -600,7 +444,6 @@ class OrganizerSettingsSerializer(SettingsSerializer):
|
|||||||
'reusable_media_type_nfc_mf0aes',
|
'reusable_media_type_nfc_mf0aes',
|
||||||
'reusable_media_type_nfc_mf0aes_autocreate_giftcard',
|
'reusable_media_type_nfc_mf0aes_autocreate_giftcard',
|
||||||
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency',
|
'reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency',
|
||||||
'reusable_media_type_nfc_mf0aes_random_uid',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -37,8 +37,6 @@ logger = logging.getLogger(__name__)
|
|||||||
class SettingsSerializer(serializers.Serializer):
|
class SettingsSerializer(serializers.Serializer):
|
||||||
default_fields = []
|
default_fields = []
|
||||||
readonly_fields = []
|
readonly_fields = []
|
||||||
default_write_permission = 'organizer.settings.general:write'
|
|
||||||
write_permission_required = {}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.changed_data = []
|
self.changed_data = []
|
||||||
@@ -60,17 +58,9 @@ class SettingsSerializer(serializers.Serializer):
|
|||||||
f._label = str(form_kwargs.get('label', fname))
|
f._label = str(form_kwargs.get('label', fname))
|
||||||
f._help_text = str(form_kwargs.get('help_text'))
|
f._help_text = str(form_kwargs.get('help_text'))
|
||||||
f.parent = self
|
f.parent = self
|
||||||
|
|
||||||
self.write_permission_required[fname] = DEFAULTS[fname].get('write_permission', self.default_write_permission)
|
|
||||||
|
|
||||||
self.fields[fname] = f
|
self.fields[fname] = f
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
for k in attrs.keys():
|
|
||||||
p = self.write_permission_required.get(k, self.default_write_permission)
|
|
||||||
if p not in self.context["permissions"]:
|
|
||||||
raise ValidationError({k: f"Setting this field requires permission {p}"})
|
|
||||||
|
|
||||||
return {k: v for k, v in attrs.items() if k not in self.readonly_fields}
|
return {k: v for k, v in attrs.items() if k not in self.readonly_fields}
|
||||||
|
|
||||||
def update(self, instance: HierarkeyProxy, validated_data):
|
def update(self, instance: HierarkeyProxy, validated_data):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -19,8 +19,6 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# 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/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
@@ -66,15 +64,14 @@ class SeatGuidField(serializers.CharField):
|
|||||||
|
|
||||||
class VoucherSerializer(I18nAwareModelSerializer):
|
class VoucherSerializer(I18nAwareModelSerializer):
|
||||||
seat = SeatGuidField(allow_null=True, required=False)
|
seat = SeatGuidField(allow_null=True, required=False)
|
||||||
budget_used = serializers.DecimalField(read_only=True, max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Voucher
|
model = Voucher
|
||||||
fields = ('id', 'created', 'code', 'max_usages', 'redeemed', 'min_usages', 'valid_until', 'block_quota',
|
fields = ('id', 'code', 'max_usages', 'redeemed', 'min_usages', 'valid_until', 'block_quota',
|
||||||
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
|
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
|
||||||
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat', 'all_addons_included',
|
'tag', 'comment', 'subevent', 'show_hidden_items', 'seat', 'all_addons_included',
|
||||||
'all_bundles_included', 'budget', 'budget_used')
|
'all_bundles_included')
|
||||||
read_only_fields = ('id', 'redeemed', 'budget_used')
|
read_only_fields = ('id', 'redeemed')
|
||||||
list_serializer_class = VoucherListSerializer
|
list_serializer_class = VoucherListSerializer
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -21,22 +21,22 @@
|
|||||||
#
|
#
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import Signal, receiver
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.api.models import ApiCall, WebHookCall
|
from pretix.api.models import ApiCall, WebHookCall
|
||||||
from pretix.base.signals import EventPluginSignal, GlobalSignal, periodic_task
|
from pretix.base.signals import EventPluginSignal, periodic_task
|
||||||
from pretix.helpers.periodic import minimum_interval
|
from pretix.helpers.periodic import minimum_interval
|
||||||
|
|
||||||
register_webhook_events = GlobalSignal()
|
register_webhook_events = Signal()
|
||||||
"""
|
"""
|
||||||
This signal is sent out to get all known webhook events. Receivers should return an
|
This signal is sent out to get all known webhook events. Receivers should return an
|
||||||
instance of a subclass of ``pretix.api.webhooks.WebhookEvent`` or a list of such
|
instance of a subclass of ``pretix.api.webhooks.WebhookEvent`` or a list of such
|
||||||
instances.
|
instances.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
register_device_security_profile = GlobalSignal()
|
register_device_security_profile = Signal()
|
||||||
"""
|
"""
|
||||||
This signal is sent out to get all known device security_profiles. Receivers should
|
This signal is sent out to get all known device security_profiles. Receivers should
|
||||||
return an instance of a subclass of ``pretix.api.auth.devicesecurity.BaseSecurityProfile``
|
return an instance of a subclass of ``pretix.api.auth.devicesecurity.BaseSecurityProfile``
|
||||||
|
|||||||
+3
-10
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -66,8 +66,6 @@ orga_router.register(r'orders', order.OrganizerOrderViewSet)
|
|||||||
orga_router.register(r'invoices', order.InvoiceViewSet)
|
orga_router.register(r'invoices', order.InvoiceViewSet)
|
||||||
orga_router.register(r'scheduled_exports', exporters.ScheduledOrganizerExportViewSet)
|
orga_router.register(r'scheduled_exports', exporters.ScheduledOrganizerExportViewSet)
|
||||||
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
|
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
|
||||||
orga_router.register(r'transactions', order.OrganizerTransactionViewSet)
|
|
||||||
orga_router.register(r'orderpositions', order.OrganizerOrderPositionViewSet, basename='orderpositions')
|
|
||||||
|
|
||||||
team_router = routers.DefaultRouter()
|
team_router = routers.DefaultRouter()
|
||||||
team_router.register(r'members', organizer.TeamMemberViewSet)
|
team_router.register(r'members', organizer.TeamMemberViewSet)
|
||||||
@@ -84,8 +82,7 @@ event_router.register(r'discounts', discount.DiscountViewSet)
|
|||||||
event_router.register(r'quotas', item.QuotaViewSet)
|
event_router.register(r'quotas', item.QuotaViewSet)
|
||||||
event_router.register(r'vouchers', voucher.VoucherViewSet)
|
event_router.register(r'vouchers', voucher.VoucherViewSet)
|
||||||
event_router.register(r'orders', order.EventOrderViewSet)
|
event_router.register(r'orders', order.EventOrderViewSet)
|
||||||
event_router.register(r'orderpositions', order.EventOrderPositionViewSet)
|
event_router.register(r'orderpositions', order.OrderPositionViewSet)
|
||||||
event_router.register(r'transactions', order.TransactionViewSet)
|
|
||||||
event_router.register(r'invoices', order.InvoiceViewSet)
|
event_router.register(r'invoices', order.InvoiceViewSet)
|
||||||
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
|
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
|
||||||
event_router.register(r'blockedsecrets', order.BlockedSecretViewSet, basename='blockedsecrets')
|
event_router.register(r'blockedsecrets', order.BlockedSecretViewSet, basename='blockedsecrets')
|
||||||
@@ -93,7 +90,6 @@ event_router.register(r'taxrules', event.TaxRuleViewSet)
|
|||||||
event_router.register(r'seats', event.SeatViewSet)
|
event_router.register(r'seats', event.SeatViewSet)
|
||||||
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
||||||
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
||||||
event_router.register(r'checkins', checkin.CheckinViewSet)
|
|
||||||
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
||||||
event_router.register(r'scheduled_exports', exporters.ScheduledEventExportViewSet)
|
event_router.register(r'scheduled_exports', exporters.ScheduledEventExportViewSet)
|
||||||
event_router.register(r'exporters', exporters.EventExportersViewSet, basename='exporters')
|
event_router.register(r'exporters', exporters.EventExportersViewSet, basename='exporters')
|
||||||
@@ -113,7 +109,6 @@ item_router = routers.DefaultRouter()
|
|||||||
item_router.register(r'variations', item.ItemVariationViewSet)
|
item_router.register(r'variations', item.ItemVariationViewSet)
|
||||||
item_router.register(r'addons', item.ItemAddOnViewSet)
|
item_router.register(r'addons', item.ItemAddOnViewSet)
|
||||||
item_router.register(r'bundles', item.ItemBundleViewSet)
|
item_router.register(r'bundles', item.ItemBundleViewSet)
|
||||||
item_router.register(r'program_times', item.ItemProgramTimeViewSet)
|
|
||||||
|
|
||||||
order_router = routers.DefaultRouter()
|
order_router = routers.DefaultRouter()
|
||||||
order_router.register(r'payments', order.PaymentViewSet)
|
order_router.register(r'payments', order.PaymentViewSet)
|
||||||
@@ -135,8 +130,6 @@ urlpatterns = [
|
|||||||
name="checkinrpc.redeem"),
|
name="checkinrpc.redeem"),
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/checkinrpc/search/$', checkin.CheckinRPCSearchView.as_view(),
|
re_path(r'^organizers/(?P<organizer>[^/]+)/checkinrpc/search/$', checkin.CheckinRPCSearchView.as_view(),
|
||||||
name="checkinrpc.search"),
|
name="checkinrpc.search"),
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/checkinrpc/annul/$', checkin.CheckinRPCAnnulView.as_view(),
|
|
||||||
name="checkinrpc.annul"),
|
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/settings/$', organizer.OrganizerSettingsView.as_view(),
|
re_path(r'^organizers/(?P<organizer>[^/]+)/settings/$', organizer.OrganizerSettingsView.as_view(),
|
||||||
name="organizer.settings"),
|
name="organizer.settings"),
|
||||||
re_path(r'^organizers/(?P<organizer>[^/]+)/giftcards/(?P<giftcard>[^/]+)/', include(giftcard_router.urls)),
|
re_path(r'^organizers/(?P<organizer>[^/]+)/giftcards/(?P<giftcard>[^/]+)/', include(giftcard_router.urls)),
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -52,8 +52,8 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
|
|||||||
ordering = ('datetime',)
|
ordering = ('datetime',)
|
||||||
ordering_fields = ('datetime', 'cart_id')
|
ordering_fields = ('datetime', 'cart_id')
|
||||||
lookup_field = 'id'
|
lookup_field = 'id'
|
||||||
permission = 'event.orders:read'
|
permission = 'can_view_orders'
|
||||||
write_permission = 'event.orders:write'
|
write_permission = 'can_change_orders'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return CartPosition.objects.filter(
|
return CartPosition.objects.filter(
|
||||||
|
|||||||
+53
-186
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -20,13 +20,12 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import operator
|
import operator
|
||||||
from datetime import timedelta
|
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError as BaseValidationError
|
from django.core.exceptions import ValidationError as BaseValidationError
|
||||||
from django.db import connection, transaction
|
from django.db import transaction
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Count, Exists, F, Max, OrderBy, OuterRef, Prefetch, Q, Subquery,
|
Count, Exists, F, Max, OrderBy, OuterRef, Prefetch, Q, Subquery,
|
||||||
prefetch_related_objects,
|
prefetch_related_objects,
|
||||||
@@ -40,24 +39,21 @@ from django.utils.translation import gettext
|
|||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from packaging.version import parse
|
from packaging.version import parse
|
||||||
from rest_framework import status, views, viewsets
|
from rest_framework import views, viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import (
|
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||||
NotFound, PermissionDenied, ValidationError,
|
|
||||||
)
|
|
||||||
from rest_framework.fields import DateTimeField
|
from rest_framework.fields import DateTimeField
|
||||||
from rest_framework.generics import ListAPIView
|
from rest_framework.generics import ListAPIView
|
||||||
from rest_framework.permissions import SAFE_METHODS
|
from rest_framework.permissions import SAFE_METHODS
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from pretix.api.serializers.checkin import (
|
from pretix.api.serializers.checkin import (
|
||||||
CheckinListSerializer, CheckinRPCAnnulInputSerializer,
|
CheckinListSerializer, CheckinRPCRedeemInputSerializer,
|
||||||
CheckinRPCRedeemInputSerializer, MiniCheckinListSerializer,
|
MiniCheckinListSerializer,
|
||||||
)
|
)
|
||||||
from pretix.api.serializers.item import QuestionSerializer
|
from pretix.api.serializers.item import QuestionSerializer
|
||||||
from pretix.api.serializers.order import (
|
from pretix.api.serializers.order import (
|
||||||
CheckinListOrderPositionSerializer, CheckinSerializer,
|
CheckinListOrderPositionSerializer, FailedCheckinSerializer,
|
||||||
FailedCheckinSerializer,
|
|
||||||
)
|
)
|
||||||
from pretix.api.views import RichOrderingFilter
|
from pretix.api.views import RichOrderingFilter
|
||||||
from pretix.api.views.order import OrderPositionFilter
|
from pretix.api.views.order import OrderPositionFilter
|
||||||
@@ -67,12 +63,9 @@ from pretix.base.models import (
|
|||||||
Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken,
|
Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken,
|
||||||
)
|
)
|
||||||
from pretix.base.models.orders import PrintLog
|
from pretix.base.models.orders import PrintLog
|
||||||
from pretix.base.permissions import AnyPermissionOf
|
|
||||||
from pretix.base.services.checkin import (
|
from pretix.base.services.checkin import (
|
||||||
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
|
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
|
||||||
)
|
)
|
||||||
from pretix.base.signals import checkin_annulled
|
|
||||||
from pretix.helpers import OF_SELF
|
|
||||||
|
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
class CheckinListFilter(FilterSet):
|
class CheckinListFilter(FilterSet):
|
||||||
@@ -98,16 +91,6 @@ with scopes_disabled():
|
|||||||
)
|
)
|
||||||
return queryset.filter(expr)
|
return queryset.filter(expr)
|
||||||
|
|
||||||
class CheckinFilter(FilterSet):
|
|
||||||
created_since = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='gte')
|
|
||||||
created_before = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='lt')
|
|
||||||
datetime_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
|
|
||||||
datetime_before = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='lt')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Checkin
|
|
||||||
fields = ['successful', 'error_reason', 'list', 'type', 'gate', 'device', 'auto_checked_in']
|
|
||||||
|
|
||||||
|
|
||||||
class CheckinListViewSet(viewsets.ModelViewSet):
|
class CheckinListViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = CheckinListSerializer
|
serializer_class = CheckinListSerializer
|
||||||
@@ -119,11 +102,11 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
def _get_permission_name(self, request):
|
def _get_permission_name(self, request):
|
||||||
if request.path.endswith('/failed_checkins/'):
|
if request.path.endswith('/failed_checkins/'):
|
||||||
return 'event.orders:checkin', 'event.orders:write'
|
return 'can_checkin_orders', 'can_change_orders'
|
||||||
elif request.method in SAFE_METHODS:
|
elif request.method in SAFE_METHODS:
|
||||||
return 'event.orders:read', 'event.orders:checkin',
|
return 'can_view_orders', 'can_checkin_orders',
|
||||||
else:
|
else:
|
||||||
return 'event.settings.general:write'
|
return 'can_change_event_settings'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = self.request.event.checkin_lists.prefetch_related(
|
qs = self.request.event.checkin_lists.prefetch_related(
|
||||||
@@ -189,15 +172,11 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
clist = self.get_object()
|
clist = self.get_object()
|
||||||
if serializer.validated_data.get('nonce'):
|
if serializer.validated_data.get('nonce'):
|
||||||
if kwargs.get('position'):
|
if kwargs.get('position'):
|
||||||
prev = kwargs['position'].all_checkins.filter(
|
prev = kwargs['position'].all_checkins.filter(nonce=serializer.validated_data['nonce']).first()
|
||||||
nonce=serializer.validated_data['nonce'],
|
|
||||||
successful=False
|
|
||||||
).first()
|
|
||||||
else:
|
else:
|
||||||
prev = clist.checkins.filter(
|
prev = clist.checkins.filter(
|
||||||
nonce=serializer.validated_data['nonce'],
|
nonce=serializer.validated_data['nonce'],
|
||||||
raw_barcode=serializer.validated_data['raw_barcode'],
|
raw_barcode=serializer.validated_data['raw_barcode'],
|
||||||
successful=False
|
|
||||||
).first()
|
).first()
|
||||||
if prev:
|
if prev:
|
||||||
# Ignore because nonce is already handled
|
# Ignore because nonce is already handled
|
||||||
@@ -386,21 +365,15 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
|||||||
|
|
||||||
qs = qs.filter(reduce(operator.or_, lists_qs))
|
qs = qs.filter(reduce(operator.or_, lists_qs))
|
||||||
|
|
||||||
prefetch_related = [
|
|
||||||
Prefetch(
|
|
||||||
lookup='checkins',
|
|
||||||
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
|
|
||||||
),
|
|
||||||
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
|
||||||
'answers', 'answers__options', 'answers__question',
|
|
||||||
]
|
|
||||||
select_related = [
|
|
||||||
'item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat'
|
|
||||||
]
|
|
||||||
|
|
||||||
if pdf_data:
|
if pdf_data:
|
||||||
qs = qs.prefetch_related(
|
qs = qs.prefetch_related(
|
||||||
# Don't add to list, we don't want to propagate to addons
|
Prefetch(
|
||||||
|
lookup='checkins',
|
||||||
|
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
|
||||||
|
),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
|
'answers', 'answers__options', 'answers__question',
|
||||||
|
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
|
||||||
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
|
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
'event',
|
'event',
|
||||||
@@ -415,39 +388,32 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
|
).select_related(
|
||||||
|
'item', 'variation', 'item__category', 'addon_to', 'order', 'order__invoice_address', 'seat'
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
qs = qs.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
lookup='checkins',
|
||||||
|
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
|
||||||
|
),
|
||||||
|
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||||
|
'answers', 'answers__options', 'answers__question',
|
||||||
|
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
||||||
|
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
|
||||||
|
|
||||||
if expand and 'subevent' in expand:
|
if expand and 'subevent' in expand:
|
||||||
prefetch_related += [
|
qs = qs.prefetch_related(
|
||||||
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
|
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
|
||||||
'subevent__seat_category_mappings', 'subevent__meta_values'
|
'subevent__seat_category_mappings', 'subevent__meta_values'
|
||||||
]
|
)
|
||||||
|
|
||||||
if expand and 'item' in expand:
|
if expand and 'item' in expand:
|
||||||
prefetch_related += [
|
qs = qs.prefetch_related('item', 'item__addons', 'item__bundles', 'item__meta_values',
|
||||||
'item', 'item__addons', 'item__bundles', 'item__meta_values',
|
'item__variations').select_related('item__tax_rule')
|
||||||
'item__variations',
|
|
||||||
]
|
|
||||||
select_related.append('item__tax_rule')
|
|
||||||
|
|
||||||
if expand and 'variation' in expand:
|
if expand and 'variation' in expand:
|
||||||
prefetch_related += [
|
qs = qs.prefetch_related('variation', 'variation__meta_values')
|
||||||
'variation', 'variation__meta_values',
|
|
||||||
]
|
|
||||||
|
|
||||||
if expand and 'addons' in expand:
|
|
||||||
prefetch_related += [
|
|
||||||
Prefetch('addons', OrderPosition.objects.prefetch_related(*prefetch_related).select_related(*select_related)),
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
prefetch_related += [
|
|
||||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
|
||||||
]
|
|
||||||
|
|
||||||
if pdf_data:
|
|
||||||
select_related.remove("order") # Don't need it twice on this queryset
|
|
||||||
|
|
||||||
qs = qs.prefetch_related(*prefetch_related).select_related(*select_related)
|
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
@@ -475,7 +441,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
|||||||
'event': op.order.event,
|
'event': op.order.event,
|
||||||
'pdf_data': pdf_data and (
|
'pdf_data': pdf_data and (
|
||||||
user if user and user.is_authenticated else auth
|
user if user and user.is_authenticated else auth
|
||||||
).has_event_permission(request.organizer, event, 'event.orders:read', request),
|
).has_event_permission(request.organizer, event, 'can_view_orders', request),
|
||||||
}
|
}
|
||||||
|
|
||||||
common_checkin_args = dict(
|
common_checkin_args = dict(
|
||||||
@@ -840,14 +806,14 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
}
|
}
|
||||||
|
|
||||||
filterset_class = CheckinOrderPositionFilter
|
filterset_class = CheckinOrderPositionFilter
|
||||||
permission = AnyPermissionOf('event.orders:read', 'event.orders:checkin')
|
permission = ('can_view_orders', 'can_checkin_orders')
|
||||||
write_permission = AnyPermissionOf('event.orders:write', 'event.orders:checkin')
|
write_permission = ('can_change_orders', 'can_checkin_orders')
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
ctx['event'] = self.request.event
|
ctx['event'] = self.request.event
|
||||||
ctx['expand'] = self.request.query_params.getlist('expand')
|
ctx['expand'] = self.request.query_params.getlist('expand')
|
||||||
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
|
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true'
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def get_filterset_kwargs(self):
|
def get_filterset_kwargs(self):
|
||||||
@@ -866,13 +832,13 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
def get_queryset(self, ignore_status=False, ignore_products=False):
|
def get_queryset(self, ignore_status=False, ignore_products=False):
|
||||||
qs = _checkin_list_position_queryset(
|
qs = _checkin_list_position_queryset(
|
||||||
[self.checkinlist],
|
[self.checkinlist],
|
||||||
ignore_status=self.request.query_params.get('ignore_status', 'false').lower() == 'true' or ignore_status,
|
ignore_status=self.request.query_params.get('ignore_status', 'false') == 'true' or ignore_status,
|
||||||
ignore_products=ignore_products,
|
ignore_products=ignore_products,
|
||||||
pdf_data=self.request.query_params.get('pdf_data', 'false').lower() == 'true',
|
pdf_data=self.request.query_params.get('pdf_data', 'false') == 'true',
|
||||||
expand=self.request.query_params.getlist('expand'),
|
expand=self.request.query_params.getlist('expand'),
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'pk' not in self.request.resolver_match.kwargs and 'event.orders:read' not in self.request.eventpermset \
|
if 'pk' not in self.request.resolver_match.kwargs and 'can_view_orders' not in self.request.eventpermset \
|
||||||
and len(self.request.query_params.get('search', '')) < 3:
|
and len(self.request.query_params.get('search', '')) < 3:
|
||||||
qs = qs.none()
|
qs = qs.none()
|
||||||
|
|
||||||
@@ -910,7 +876,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
expand=self.request.query_params.getlist('expand'),
|
expand=self.request.query_params.getlist('expand'),
|
||||||
pdf_data=self.request.query_params.get('pdf_data', 'false').lower() == 'true',
|
pdf_data=self.request.query_params.get('pdf_data', 'false') == 'true',
|
||||||
questions_supported=self.request.data.get('questions_supported', True),
|
questions_supported=self.request.data.get('questions_supported', True),
|
||||||
canceled_supported=self.request.data.get('canceled_supported', False),
|
canceled_supported=self.request.data.get('canceled_supported', False),
|
||||||
request=self.request, # this is not clean, but we need it in the serializers for URL generation
|
request=self.request, # this is not clean, but we need it in the serializers for URL generation
|
||||||
@@ -921,9 +887,9 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
class CheckinRPCRedeemView(views.APIView):
|
class CheckinRPCRedeemView(views.APIView):
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||||
events = self.request.auth.get_events_with_permission(('event.orders:write', 'event.orders:checkin'))
|
events = self.request.auth.get_events_with_permission(('can_change_orders', 'can_checkin_orders'))
|
||||||
elif self.request.user.is_authenticated:
|
elif self.request.user.is_authenticated:
|
||||||
events = self.request.user.get_events_with_permission(('event.orders:write', 'event.orders:checkin'), self.request).filter(
|
events = self.request.user.get_events_with_permission(('can_change_orders', 'can_checkin_orders'), self.request).filter(
|
||||||
organizer=self.request.organizer
|
organizer=self.request.organizer
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -945,7 +911,7 @@ class CheckinRPCRedeemView(views.APIView):
|
|||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
expand=self.request.query_params.getlist('expand'),
|
expand=self.request.query_params.getlist('expand'),
|
||||||
pdf_data=self.request.query_params.get('pdf_data', 'false').lower() == 'true',
|
pdf_data=self.request.query_params.get('pdf_data', 'false') == 'true',
|
||||||
questions_supported=s.validated_data['questions_supported'],
|
questions_supported=s.validated_data['questions_supported'],
|
||||||
use_order_locale=s.validated_data['use_order_locale'],
|
use_order_locale=s.validated_data['use_order_locale'],
|
||||||
canceled_supported=True,
|
canceled_supported=True,
|
||||||
@@ -984,16 +950,15 @@ class CheckinRPCSearchView(ListAPIView):
|
|||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
ctx['expand'] = self.request.query_params.getlist('expand')
|
ctx['expand'] = self.request.query_params.getlist('expand')
|
||||||
ctx['organizer'] = self.request.organizer
|
|
||||||
ctx['pdf_data'] = False
|
ctx['pdf_data'] = False
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def lists(self):
|
def lists(self):
|
||||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||||
events = self.request.auth.get_events_with_permission(('event.orders:read', 'event.orders:checkin'))
|
events = self.request.auth.get_events_with_permission(('can_view_orders', 'can_checkin_orders'))
|
||||||
elif self.request.user.is_authenticated:
|
elif self.request.user.is_authenticated:
|
||||||
events = self.request.user.get_events_with_permission(('event.orders:read', 'event.orders:checkin'), self.request).filter(
|
events = self.request.user.get_events_with_permission(('can_view_orders', 'can_checkin_orders'), self.request).filter(
|
||||||
organizer=self.request.organizer
|
organizer=self.request.organizer
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -1010,9 +975,9 @@ class CheckinRPCSearchView(ListAPIView):
|
|||||||
@cached_property
|
@cached_property
|
||||||
def has_full_access_permission(self):
|
def has_full_access_permission(self):
|
||||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||||
events = self.request.auth.get_events_with_permission('event.orders:read')
|
events = self.request.auth.get_events_with_permission('can_view_orders')
|
||||||
elif self.request.user.is_authenticated:
|
elif self.request.user.is_authenticated:
|
||||||
events = self.request.user.get_events_with_permission('event.orders:read', self.request).filter(
|
events = self.request.user.get_events_with_permission('can_view_orders', self.request).filter(
|
||||||
organizer=self.request.organizer
|
organizer=self.request.organizer
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -1024,9 +989,9 @@ class CheckinRPCSearchView(ListAPIView):
|
|||||||
def get_queryset(self, ignore_status=False, ignore_products=False):
|
def get_queryset(self, ignore_status=False, ignore_products=False):
|
||||||
qs = _checkin_list_position_queryset(
|
qs = _checkin_list_position_queryset(
|
||||||
self.lists,
|
self.lists,
|
||||||
ignore_status=self.request.query_params.get('ignore_status', 'false').lower() == 'true' or ignore_status,
|
ignore_status=self.request.query_params.get('ignore_status', 'false') == 'true' or ignore_status,
|
||||||
ignore_products=ignore_products,
|
ignore_products=ignore_products,
|
||||||
pdf_data=self.request.query_params.get('pdf_data', 'false').lower() == 'true',
|
pdf_data=self.request.query_params.get('pdf_data', 'false') == 'true',
|
||||||
expand=self.request.query_params.getlist('expand'),
|
expand=self.request.query_params.getlist('expand'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1034,101 +999,3 @@ class CheckinRPCSearchView(ListAPIView):
|
|||||||
qs = qs.none()
|
qs = qs.none()
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
class CheckinRPCAnnulView(views.APIView):
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
|
||||||
events = self.request.auth.get_events_with_permission(('event.orders:write', 'event.orders:checkin'))
|
|
||||||
elif self.request.user.is_authenticated:
|
|
||||||
events = self.request.user.get_events_with_permission(('event.orders:write', 'event.orders:checkin'), self.request).filter(
|
|
||||||
organizer=self.request.organizer
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError("unknown authentication method")
|
|
||||||
|
|
||||||
s = CheckinRPCAnnulInputSerializer(data=request.data, context={'events': events})
|
|
||||||
s.is_valid(raise_exception=True)
|
|
||||||
|
|
||||||
with transaction.atomic():
|
|
||||||
try:
|
|
||||||
qs = Checkin.all.all()
|
|
||||||
if isinstance(request.auth, Device):
|
|
||||||
qs = qs.filter(device=request.auth)
|
|
||||||
ci = qs.select_for_update(
|
|
||||||
of=OF_SELF,
|
|
||||||
).select_related("position", "position__order", "position__order__event").get(
|
|
||||||
list__in=s.validated_data['lists'],
|
|
||||||
nonce=s.validated_data['nonce'],
|
|
||||||
)
|
|
||||||
if connection.features.has_select_for_update_of and ci.position_id:
|
|
||||||
# Lock position as well, can't do it with of= above because relation is nullable
|
|
||||||
OrderPosition.objects.select_for_update(of=OF_SELF).get(pk=ci.position_id)
|
|
||||||
|
|
||||||
if not ci.successful or not ci.position:
|
|
||||||
raise ValidationError("Cannot annul an unsuccessful checkin")
|
|
||||||
except Checkin.DoesNotExist:
|
|
||||||
raise NotFound("No check-in found based on nonce")
|
|
||||||
except Checkin.MultipleObjectsReturned:
|
|
||||||
raise ValidationError("Multiple check-ins found based on nonce")
|
|
||||||
|
|
||||||
annulment_time = s.validated_data.get("datetime") or now()
|
|
||||||
|
|
||||||
if annulment_time - ci.datetime > timedelta(minutes=15):
|
|
||||||
# Compare to sent datetime, which makes this cheatable, but allows offline annulment of checkins
|
|
||||||
ci.position.order.log_action('pretix.event.checkin.annulment.ignored', data={
|
|
||||||
'checkin': ci.pk,
|
|
||||||
'position': ci.position.id,
|
|
||||||
'positionid': ci.position.positionid,
|
|
||||||
'datetime': annulment_time,
|
|
||||||
'error_explanation': s.validated_data.get("error_explanation"),
|
|
||||||
'type': ci.type,
|
|
||||||
'list': ci.list_id,
|
|
||||||
}, user=request.user, auth=request.auth)
|
|
||||||
return Response({
|
|
||||||
"non_field_errors": ["Annulment is not allowed more than 15 minutes after check-in"]
|
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
if ci.device and ci.device != request.auth:
|
|
||||||
return Response({
|
|
||||||
"non_field_errors": ["Annulment is only allowed from the same device"]
|
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
ci.successful = False
|
|
||||||
ci.error_reason = Checkin.REASON_ANNULLED
|
|
||||||
ci.error_explanation = s.validated_data.get("error_explanation")
|
|
||||||
ci.save(update_fields=["successful", "error_reason", "error_explanation"])
|
|
||||||
ci.position.order.log_action('pretix.event.checkin.annulled', data={
|
|
||||||
'checkin': ci.pk,
|
|
||||||
'position': ci.position.id,
|
|
||||||
'positionid': ci.position.positionid,
|
|
||||||
'datetime': annulment_time,
|
|
||||||
'error_explanation': s.validated_data.get("error_explanation"),
|
|
||||||
'type': ci.type,
|
|
||||||
'list': ci.list_id,
|
|
||||||
}, user=request.user, auth=request.auth)
|
|
||||||
checkin_annulled.send(ci.position.order.event, checkin=ci)
|
|
||||||
|
|
||||||
return Response({"status": "ok"}, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
|
|
||||||
class CheckinViewSet(viewsets.ReadOnlyModelViewSet):
|
|
||||||
serializer_class = CheckinSerializer
|
|
||||||
queryset = Checkin.all.none()
|
|
||||||
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
|
|
||||||
filterset_class = CheckinFilter
|
|
||||||
ordering = ('created', 'id')
|
|
||||||
ordering_fields = ('created', 'datetime', 'id',)
|
|
||||||
permission = 'event.orders:read'
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
qs = Checkin.all.filter(list__event=self.request.event).select_related(
|
|
||||||
"position",
|
|
||||||
"device",
|
|
||||||
)
|
|
||||||
return qs
|
|
||||||
|
|
||||||
def get_serializer_context(self):
|
|
||||||
ctx = super().get_serializer_context()
|
|
||||||
ctx['event'] = self.request.event
|
|
||||||
return ctx
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -57,7 +57,7 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('position', 'id')
|
ordering = ('position', 'id')
|
||||||
permission = None
|
permission = None
|
||||||
write_permission = 'event.items:write'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.discounts.prefetch_related(
|
return self.request.event.discounts.prefetch_related(
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -281,11 +281,6 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
new_event = serializer.save(organizer=self.request.organizer)
|
new_event = serializer.save(organizer=self.request.organizer)
|
||||||
|
|
||||||
if copy_from:
|
if copy_from:
|
||||||
perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken))
|
|
||||||
else self.request.user)
|
|
||||||
if not copy_from.allow_copy_data(self.request.organizer, perm_holder):
|
|
||||||
raise PermissionDenied("Not sufficient permission on source event to copy")
|
|
||||||
|
|
||||||
new_event.copy_data_from(copy_from, skip_meta_data='meta_data' in serializer.validated_data)
|
new_event.copy_data_from(copy_from, skip_meta_data='meta_data' in serializer.validated_data)
|
||||||
|
|
||||||
if plugins is not None:
|
if plugins is not None:
|
||||||
@@ -346,24 +341,15 @@ class CloneEventViewSet(viewsets.ModelViewSet):
|
|||||||
lookup_field = 'slug'
|
lookup_field = 'slug'
|
||||||
lookup_url_kwarg = 'event'
|
lookup_url_kwarg = 'event'
|
||||||
http_method_names = ['post']
|
http_method_names = ['post']
|
||||||
write_permission = 'event.settings.general:write'
|
write_permission = 'can_create_events'
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
ctx['event'] = Event.objects.get(slug=self.kwargs['event'], organizer=self.request.organizer)
|
ctx['event'] = self.kwargs['event']
|
||||||
ctx['organizer'] = self.request.organizer
|
ctx['organizer'] = self.request.organizer
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
# Weird edge case: Requires settings permission on the event (to read) but also on the organizer (two write)
|
|
||||||
perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken))
|
|
||||||
else self.request.user)
|
|
||||||
if not perm_holder.has_organizer_permission(self.request.organizer, "organizer.events:create", request=self.request):
|
|
||||||
raise PermissionDenied("No permission to create events")
|
|
||||||
|
|
||||||
if not serializer.context['event'].allow_copy_data(self.request.organizer, perm_holder):
|
|
||||||
raise PermissionDenied("Not sufficient permission on source event to copy")
|
|
||||||
|
|
||||||
serializer.save(organizer=self.request.organizer)
|
serializer.save(organizer=self.request.organizer)
|
||||||
|
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
@@ -440,7 +426,7 @@ with scopes_disabled():
|
|||||||
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = SubEventSerializer
|
serializer_class = SubEventSerializer
|
||||||
queryset = SubEvent.objects.none()
|
queryset = SubEvent.objects.none()
|
||||||
write_permission = 'event.subevents:write'
|
write_permission = 'can_change_event_settings'
|
||||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
||||||
ordering = ('date_from',)
|
ordering = ('date_from',)
|
||||||
ordering_fields = ('id', 'date_from', 'last_modified')
|
ordering_fields = ('id', 'date_from', 'last_modified')
|
||||||
@@ -560,7 +546,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = TaxRuleSerializer
|
serializer_class = TaxRuleSerializer
|
||||||
queryset = TaxRule.objects.none()
|
queryset = TaxRule.objects.none()
|
||||||
write_permission = 'event.settings.tax:write'
|
write_permission = 'can_change_event_settings'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.tax_rules.all()
|
return self.request.event.tax_rules.all()
|
||||||
@@ -594,16 +580,11 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
def get_serializer_context(self):
|
|
||||||
ctx = super().get_serializer_context()
|
|
||||||
ctx["event"] = self.request.event
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
|
|
||||||
class ItemMetaPropertiesViewSet(viewsets.ModelViewSet):
|
class ItemMetaPropertiesViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = ItemMetaPropertiesSerializer
|
serializer_class = ItemMetaPropertiesSerializer
|
||||||
queryset = ItemMetaProperty.objects.none()
|
queryset = ItemMetaProperty.objects.none()
|
||||||
write_permission = 'event.settings.general:write'
|
write_permission = 'can_change_event_settings'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = self.request.event.item_meta_properties.all()
|
qs = self.request.event.item_meta_properties.all()
|
||||||
@@ -650,18 +631,19 @@ class ItemMetaPropertiesViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
class EventSettingsView(views.APIView):
|
class EventSettingsView(views.APIView):
|
||||||
permission = None
|
permission = None
|
||||||
write_permission = 'event.settings.general:write'
|
write_permission = 'can_change_event_settings'
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if isinstance(request.auth, Device):
|
if isinstance(request.auth, Device):
|
||||||
s = DeviceEventSettingsSerializer(instance=request.event.settings, event=request.event, context={
|
s = DeviceEventSettingsSerializer(instance=request.event.settings, event=request.event, context={
|
||||||
'request': request, 'permissions': request.eventpermset
|
'request': request
|
||||||
|
})
|
||||||
|
elif 'can_change_event_settings' in request.eventpermset:
|
||||||
|
s = EventSettingsSerializer(instance=request.event.settings, event=request.event, context={
|
||||||
|
'request': request
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
s = EventSettingsSerializer(instance=request.event.settings, event=request.event, context={
|
raise PermissionDenied()
|
||||||
'request': request, 'permissions': request.eventpermset,
|
|
||||||
})
|
|
||||||
|
|
||||||
if 'explain' in request.GET:
|
if 'explain' in request.GET:
|
||||||
return Response({
|
return Response({
|
||||||
fname: {
|
fname: {
|
||||||
@@ -675,7 +657,7 @@ class EventSettingsView(views.APIView):
|
|||||||
|
|
||||||
def patch(self, request, *wargs, **kwargs):
|
def patch(self, request, *wargs, **kwargs):
|
||||||
s = EventSettingsSerializer(instance=request.event.settings, data=request.data, partial=True,
|
s = EventSettingsSerializer(instance=request.event.settings, data=request.data, partial=True,
|
||||||
event=request.event, context={'request': request, 'permissions': request.eventpermset})
|
event=request.event, context={'request': request})
|
||||||
s.is_valid(raise_exception=True)
|
s.is_valid(raise_exception=True)
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
s.save()
|
s.save()
|
||||||
@@ -687,7 +669,7 @@ class EventSettingsView(views.APIView):
|
|||||||
)
|
)
|
||||||
s = EventSettingsSerializer(
|
s = EventSettingsSerializer(
|
||||||
instance=request.event.settings, event=request.event, context={
|
instance=request.event.settings, event=request.event, context={
|
||||||
'request': request, 'permissions': request.eventpermset
|
'request': request
|
||||||
})
|
})
|
||||||
return Response(s.data)
|
return Response(s.data)
|
||||||
|
|
||||||
@@ -714,7 +696,7 @@ class SeatFilter(FilterSet):
|
|||||||
class SeatViewSet(ConditionalListView, viewsets.ModelViewSet):
|
class SeatViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = SeatSerializer
|
serializer_class = SeatSerializer
|
||||||
queryset = Seat.objects.none()
|
queryset = Seat.objects.none()
|
||||||
write_permission = 'event.settings.general:write'
|
write_permission = 'can_change_event_settings'
|
||||||
filter_backends = (DjangoFilterBackend, )
|
filter_backends = (DjangoFilterBackend, )
|
||||||
filterset_class = SeatFilter
|
filterset_class = SeatFilter
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -40,12 +40,12 @@ from pretix.api.serializers.exporters import (
|
|||||||
)
|
)
|
||||||
from pretix.base.exporter import OrganizerLevelExportMixin
|
from pretix.base.exporter import OrganizerLevelExportMixin
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedFile, Device, ScheduledEventExport, ScheduledOrganizerExport,
|
CachedFile, Device, Event, ScheduledEventExport, ScheduledOrganizerExport,
|
||||||
TeamAPIToken,
|
TeamAPIToken,
|
||||||
)
|
)
|
||||||
from pretix.base.models.organizer import TeamQuerySet
|
from pretix.base.services.export import export, multiexport
|
||||||
from pretix.base.services.export import (
|
from pretix.base.signals import (
|
||||||
export, init_event_exporters, init_organizer_exporters, multiexport,
|
register_data_exporters, register_multievent_data_exporters,
|
||||||
)
|
)
|
||||||
from pretix.helpers.http import ChunkBasedFileResponse
|
from pretix.helpers.http import ChunkBasedFileResponse
|
||||||
|
|
||||||
@@ -74,11 +74,6 @@ 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")
|
||||||
@@ -111,11 +106,10 @@ class ExportersMixin:
|
|||||||
@action(detail=True, methods=['POST'])
|
@action(detail=True, methods=['POST'])
|
||||||
def run(self, *args, **kwargs):
|
def run(self, *args, **kwargs):
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
serializer = JobRunSerializer(exporter=instance, data=self.request.data)
|
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=True)
|
cf = CachedFile(web_download=False)
|
||||||
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()
|
||||||
@@ -136,34 +130,27 @@ class ExportersMixin:
|
|||||||
|
|
||||||
|
|
||||||
class EventExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
class EventExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
||||||
permission = None
|
permission = 'can_view_orders'
|
||||||
|
|
||||||
|
def get_serializer_kwargs(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def exporters(self):
|
def exporters(self):
|
||||||
raw_exporters = list(init_event_exporters(
|
|
||||||
event=self.request.event,
|
|
||||||
user=self.request.user if self.request.user and self.request.user.is_authenticated else None,
|
|
||||||
token=self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None,
|
|
||||||
device=self.request.auth if isinstance(self.request.auth, Device) else None,
|
|
||||||
request=self.request,
|
|
||||||
))
|
|
||||||
exporters = []
|
exporters = []
|
||||||
|
responses = register_data_exporters.send(self.request.event)
|
||||||
|
raw_exporters = [response(self.request.event, self.request.organizer) for r, response in responses if response]
|
||||||
|
raw_exporters = [
|
||||||
|
ex for ex in raw_exporters
|
||||||
|
if ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
|
||||||
|
]
|
||||||
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
|
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
|
||||||
ex._serializer = JobRunSerializer(exporter=ex)
|
ex._serializer = JobRunSerializer(exporter=ex)
|
||||||
exporters.append(ex)
|
exporters.append(ex)
|
||||||
return exporters
|
return exporters
|
||||||
|
|
||||||
def do_export(self, cf, instance, data):
|
def do_export(self, cf, instance, data):
|
||||||
return export.apply_async(args=(
|
return export.apply_async(args=(self.request.event.id, str(cf.id), instance.identifier, data))
|
||||||
self.request.event.id,
|
|
||||||
), kwargs={
|
|
||||||
'user': self.request.user.pk if self.request.user and self.request.user.is_authenticated else None,
|
|
||||||
'token': self.request.auth.pk if isinstance(self.request.auth, TeamAPIToken) else None,
|
|
||||||
'device': self.request.auth.pk if isinstance(self.request.auth, Device) else None,
|
|
||||||
'fileid': str(cf.id),
|
|
||||||
'provider': instance.identifier,
|
|
||||||
'form_data': data,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
||||||
@@ -171,23 +158,47 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def exporters(self):
|
def exporters(self):
|
||||||
raw_exporters = list(init_organizer_exporters(
|
|
||||||
organizer=self.request.organizer,
|
|
||||||
user=self.request.user if self.request.user and self.request.user.is_authenticated else None,
|
|
||||||
token=self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None,
|
|
||||||
device=self.request.auth if isinstance(self.request.auth, Device) else None,
|
|
||||||
request=self.request,
|
|
||||||
))
|
|
||||||
exporters = []
|
exporters = []
|
||||||
|
if isinstance(self.request.auth, (Device, TeamAPIToken)):
|
||||||
|
perm_holder = self.request.auth
|
||||||
|
else:
|
||||||
|
perm_holder = self.request.user
|
||||||
|
events = perm_holder.get_events_with_permission('can_view_orders', request=self.request).filter(
|
||||||
|
organizer=self.request.organizer
|
||||||
|
)
|
||||||
|
responses = register_multievent_data_exporters.send(self.request.organizer)
|
||||||
|
raw_exporters = [
|
||||||
|
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else events, self.request.organizer)
|
||||||
|
for r, response in responses
|
||||||
|
if response
|
||||||
|
]
|
||||||
|
raw_exporters = [
|
||||||
|
ex for ex in raw_exporters
|
||||||
|
if (
|
||||||
|
not isinstance(ex, OrganizerLevelExportMixin) or
|
||||||
|
perm_holder.has_organizer_permission(self.request.organizer, ex.organizer_required_permission, self.request)
|
||||||
|
) and ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
|
||||||
|
]
|
||||||
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
|
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
|
||||||
ex._serializer = JobRunSerializer(exporter=ex)
|
ex._serializer = JobRunSerializer(exporter=ex, events=events)
|
||||||
exporters.append(ex)
|
exporters.append(ex)
|
||||||
return exporters
|
return exporters
|
||||||
|
|
||||||
|
def get_serializer_kwargs(self):
|
||||||
|
if isinstance(self.request.auth, (Device, TeamAPIToken)):
|
||||||
|
perm_holder = self.request.auth
|
||||||
|
else:
|
||||||
|
perm_holder = self.request.user
|
||||||
|
return {
|
||||||
|
'events': perm_holder.get_events_with_permission('can_view_orders', request=self.request).filter(
|
||||||
|
organizer=self.request.organizer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
def do_export(self, cf, instance, data):
|
def do_export(self, cf, instance, data):
|
||||||
return multiexport.apply_async(kwargs={
|
return multiexport.apply_async(kwargs={
|
||||||
'organizer': self.request.organizer.id,
|
'organizer': self.request.organizer.id,
|
||||||
'user': self.request.user.id if self.request.user and self.request.user.is_authenticated else None,
|
'user': self.request.user.id if self.request.user.is_authenticated else None,
|
||||||
'token': self.request.auth.pk if isinstance(self.request.auth, TeamAPIToken) else None,
|
'token': self.request.auth.pk if isinstance(self.request.auth, TeamAPIToken) else None,
|
||||||
'device': self.request.auth.pk if isinstance(self.request.auth, Device) else None,
|
'device': self.request.auth.pk if isinstance(self.request.auth, Device) else None,
|
||||||
'fileid': str(cf.id),
|
'fileid': str(cf.id),
|
||||||
@@ -205,11 +216,11 @@ class ScheduledExportersViewSet(viewsets.ModelViewSet):
|
|||||||
class ScheduledEventExportViewSet(ScheduledExportersViewSet):
|
class ScheduledEventExportViewSet(ScheduledExportersViewSet):
|
||||||
serializer_class = ScheduledEventExportSerializer
|
serializer_class = ScheduledEventExportSerializer
|
||||||
queryset = ScheduledEventExport.objects.none()
|
queryset = ScheduledEventExport.objects.none()
|
||||||
permission = None
|
permission = 'can_view_orders'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
|
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
|
||||||
if not perm_holder.has_event_permission(self.request.organizer, self.request.event, 'event.settings.general:write',
|
if not perm_holder.has_event_permission(self.request.organizer, self.request.event, 'can_change_event_settings',
|
||||||
request=self.request):
|
request=self.request):
|
||||||
if self.request.user.is_authenticated:
|
if self.request.user.is_authenticated:
|
||||||
qs = self.request.event.scheduled_exports.filter(owner=self.request.user)
|
qs = self.request.event.scheduled_exports.filter(owner=self.request.user)
|
||||||
@@ -241,28 +252,11 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def exporters(self):
|
def exporters(self):
|
||||||
exporters = list(init_event_exporters(
|
responses = register_data_exporters.send(self.request.event)
|
||||||
event=self.request.event,
|
exporters = [response(self.request.event, self.request.organizer) for r, response in responses if response]
|
||||||
user=self.request.user if self.request.user and self.request.user.is_authenticated else None,
|
|
||||||
token=self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None,
|
|
||||||
device=self.request.auth if isinstance(self.request.auth, Device) else None,
|
|
||||||
request=self.request,
|
|
||||||
))
|
|
||||||
return {e.identifier: e for e in exporters}
|
return {e.identifier: e for e in exporters}
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
if not self.request.user.is_authenticated or self.request.user != serializer.instance.owner:
|
|
||||||
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
|
|
||||||
# user B has settings permission (= they can see the export configuration), but not enough permission
|
|
||||||
# to run the export themselves. Without this check, user B could modify the export and add themselves
|
|
||||||
# as a recipient. Thereby, user B would gain access to data they can't have.
|
|
||||||
exporter = self.exporters.get(serializer.instance.export_identifier)
|
|
||||||
if not exporter:
|
|
||||||
raise PermissionDenied("No access to exporter.")
|
|
||||||
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
|
|
||||||
if not perm_holder.has_event_permission(self.request.organizer, self.request.event, exporter.get_required_event_permission()):
|
|
||||||
raise PermissionDenied("No permission to edit exports you could not run.")
|
|
||||||
|
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
serializer.instance.compute_next_run()
|
serializer.instance.compute_next_run()
|
||||||
serializer.instance.error_counter = 0
|
serializer.instance.error_counter = 0
|
||||||
@@ -291,7 +285,7 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
|
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
|
||||||
if not perm_holder.has_organizer_permission(self.request.organizer, 'organizer.settings.general:write',
|
if not perm_holder.has_organizer_permission(self.request.organizer, 'can_change_organizer_settings',
|
||||||
request=self.request):
|
request=self.request):
|
||||||
if self.request.user.is_authenticated:
|
if self.request.user.is_authenticated:
|
||||||
qs = self.request.organizer.scheduled_exports.filter(owner=self.request.user)
|
qs = self.request.organizer.scheduled_exports.filter(owner=self.request.user)
|
||||||
@@ -321,55 +315,26 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
|
|||||||
ctx['exporters'] = self.exporters
|
ctx['exporters'] = self.exporters
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def events(self):
|
||||||
|
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||||
|
return self.request.auth.get_events_with_permission('can_view_orders')
|
||||||
|
elif self.request.user.is_authenticated:
|
||||||
|
return self.request.user.get_events_with_permission('can_view_orders', self.request).filter(
|
||||||
|
organizer=self.request.organizer
|
||||||
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def exporters(self):
|
def exporters(self):
|
||||||
exporters = list(init_organizer_exporters(
|
responses = register_multievent_data_exporters.send(self.request.organizer)
|
||||||
organizer=self.request.organizer,
|
exporters = [
|
||||||
user=self.request.user if self.request.user and self.request.user.is_authenticated else None,
|
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else self.events,
|
||||||
token=self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None,
|
self.request.organizer)
|
||||||
device=self.request.auth if isinstance(self.request.auth, Device) else None,
|
for r, response in responses if response
|
||||||
request=self.request,
|
]
|
||||||
))
|
|
||||||
return {e.identifier: e for e in exporters}
|
return {e.identifier: e for e in exporters}
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
if not self.request.user.is_authenticated or self.request.user != serializer.instance.owner:
|
|
||||||
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
|
|
||||||
# user B has settings permission (= they can see the export configuration), but not enough permission
|
|
||||||
# to run the export themselves. Without this check, user B could modify the export and add themselves
|
|
||||||
# as a recipient. Thereby, user B would gain access to data they can't have.
|
|
||||||
exporter = self.exporters.get(serializer.instance.export_identifier)
|
|
||||||
if not exporter:
|
|
||||||
raise PermissionDenied("No access to exporter.")
|
|
||||||
perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken))
|
|
||||||
else self.request.user)
|
|
||||||
if isinstance(exporter, OrganizerLevelExportMixin):
|
|
||||||
if not perm_holder.has_organizer_permission(
|
|
||||||
self.request.organizer, exporter.get_required_organizer_permission(), request=self.request,
|
|
||||||
):
|
|
||||||
raise PermissionDenied("No permission to edit exports you could not run.")
|
|
||||||
else:
|
|
||||||
if serializer.instance.export_form_data.get("all_events", False):
|
|
||||||
if isinstance(self.request.auth, Device):
|
|
||||||
if not self.request.auth.all_events:
|
|
||||||
raise PermissionDenied("No permission to edit exports you could not run.")
|
|
||||||
elif isinstance(self.request.auth, TeamAPIToken):
|
|
||||||
if not self.request.auth.team.all_events:
|
|
||||||
raise PermissionDenied("No permission to edit exports you could not run.")
|
|
||||||
elif self.request.user.is_authenticated:
|
|
||||||
if not self.request.user.teams.filter(
|
|
||||||
TeamQuerySet.event_permission_q(exporter.get_required_event_permission()),
|
|
||||||
all_events=True,
|
|
||||||
).exists():
|
|
||||||
raise PermissionDenied("No permission to edit exports you could not run.")
|
|
||||||
else:
|
|
||||||
events_selected = serializer.instance.export_form_data.get("events", [])
|
|
||||||
events_permission = set(perm_holder.get_events_with_permission(
|
|
||||||
exporter.get_required_event_permission(), request=self.request
|
|
||||||
).values_list("pk", flat=True))
|
|
||||||
if not all(e in events_permission for e in events_selected):
|
|
||||||
raise PermissionDenied("No permission to edit exports you could not run.")
|
|
||||||
|
|
||||||
serializer.save(organizer=self.request.organizer)
|
serializer.save(organizer=self.request.organizer)
|
||||||
serializer.instance.compute_next_run()
|
serializer.instance.compute_next_run()
|
||||||
serializer.instance.error_counter = 0
|
serializer.instance.error_counter = 0
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -40,19 +40,19 @@ from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
|||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from pretix.api.pagination import TotalOrderingFilter
|
from pretix.api.pagination import TotalOrderingFilter
|
||||||
from pretix.api.serializers.item import (
|
from pretix.api.serializers.item import (
|
||||||
ItemAddOnSerializer, ItemBundleSerializer, ItemCategorySerializer,
|
ItemAddOnSerializer, ItemBundleSerializer, ItemCategorySerializer,
|
||||||
ItemProgramTimeSerializer, ItemSerializer, ItemVariationSerializer,
|
ItemSerializer, ItemVariationSerializer, QuestionOptionSerializer,
|
||||||
QuestionOptionSerializer, QuestionSerializer, QuotaSerializer,
|
QuestionSerializer, QuotaSerializer,
|
||||||
)
|
)
|
||||||
from pretix.api.views import ConditionalListView
|
from pretix.api.views import ConditionalListView
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemProgramTime,
|
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation,
|
||||||
ItemVariation, Question, QuestionOption, Quota,
|
Question, QuestionOption, Quota,
|
||||||
)
|
)
|
||||||
from pretix.base.services.quotas import QuotaAvailability
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
@@ -99,14 +99,14 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
ordering = ('position', 'id')
|
ordering = ('position', 'id')
|
||||||
filterset_class = ItemFilter
|
filterset_class = ItemFilter
|
||||||
permission = None
|
permission = None
|
||||||
write_permission = 'event.items:write'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.items.select_related('tax_rule').prefetch_related(
|
return self.request.event.items.select_related('tax_rule').prefetch_related(
|
||||||
'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', 'program_times'
|
'limit_sales_channels', 'variations__limit_sales_channels',
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
@@ -163,7 +163,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
|||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('id',)
|
ordering = ('id',)
|
||||||
permission = None
|
permission = None
|
||||||
write_permission = 'event.items:write'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def item(self):
|
def item(self):
|
||||||
@@ -234,7 +234,7 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
|
|||||||
ordering_fields = ('id',)
|
ordering_fields = ('id',)
|
||||||
ordering = ('id',)
|
ordering = ('id',)
|
||||||
permission = None
|
permission = None
|
||||||
write_permission = 'event.items:write'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def item(self):
|
def item(self):
|
||||||
@@ -279,59 +279,6 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ItemProgramTimeViewSet(viewsets.ModelViewSet):
|
|
||||||
serializer_class = ItemProgramTimeSerializer
|
|
||||||
queryset = ItemProgramTime.objects.none()
|
|
||||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter,)
|
|
||||||
ordering_fields = ('id',)
|
|
||||||
ordering = ('id',)
|
|
||||||
permission = None
|
|
||||||
write_permission = 'event.items:write'
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def item(self):
|
|
||||||
return get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
if self.request.event.has_subevents:
|
|
||||||
raise ValidationError('You cannot use program times on an event series.')
|
|
||||||
return self.item.program_times.all()
|
|
||||||
|
|
||||||
def get_serializer_context(self):
|
|
||||||
ctx = super().get_serializer_context()
|
|
||||||
ctx['event'] = self.request.event
|
|
||||||
ctx['item'] = self.item
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
|
||||||
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
|
||||||
serializer.save(item=item)
|
|
||||||
item.log_action(
|
|
||||||
'pretix.event.item.program_times.added',
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
|
||||||
)
|
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
|
||||||
serializer.save(event=self.request.event)
|
|
||||||
serializer.instance.item.log_action(
|
|
||||||
'pretix.event.item.program_times.changed',
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
|
||||||
)
|
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
|
||||||
super().perform_destroy(instance)
|
|
||||||
instance.item.log_action(
|
|
||||||
'pretix.event.item.program_times.removed',
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
data={'start': instance.start, 'end': instance.end}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ItemAddOnViewSet(viewsets.ModelViewSet):
|
class ItemAddOnViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = ItemAddOnSerializer
|
serializer_class = ItemAddOnSerializer
|
||||||
queryset = ItemAddOn.objects.none()
|
queryset = ItemAddOn.objects.none()
|
||||||
@@ -339,7 +286,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
|||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('id',)
|
ordering = ('id',)
|
||||||
permission = None
|
permission = None
|
||||||
write_permission = 'event.items:write'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def item(self):
|
def item(self):
|
||||||
@@ -398,7 +345,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('position', 'id')
|
ordering = ('position', 'id')
|
||||||
permission = None
|
permission = None
|
||||||
write_permission = 'event.items:write'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.categories.all()
|
return self.request.event.categories.all()
|
||||||
@@ -453,7 +400,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('position', 'id')
|
ordering = ('position', 'id')
|
||||||
permission = None
|
permission = None
|
||||||
write_permission = 'event.items:write'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.questions.prefetch_related('options').all()
|
return self.request.event.questions.prefetch_related('options').all()
|
||||||
@@ -497,7 +444,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
|||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('position',)
|
ordering = ('position',)
|
||||||
permission = None
|
permission = None
|
||||||
write_permission = 'event.items:write'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
q = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
|
q = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
|
||||||
@@ -564,10 +511,10 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
ordering_fields = ('id', 'size')
|
ordering_fields = ('id', 'size')
|
||||||
ordering = ('id',)
|
ordering = ('id',)
|
||||||
permission = None
|
permission = None
|
||||||
write_permission = 'event.items:write'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.quotas.select_related('subevent').prefetch_related('items', 'variations').all()
|
return self.request.event.quotas.all()
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
queryset = self.filter_queryset(self.get_queryset()).distinct()
|
queryset = self.filter_queryset(self.get_queryset()).distinct()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# 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.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -62,8 +62,8 @@ with scopes_disabled():
|
|||||||
class ReusableMediaViewSet(viewsets.ModelViewSet):
|
class ReusableMediaViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = ReusableMediaSerializer
|
serializer_class = ReusableMediaSerializer
|
||||||
queryset = ReusableMedium.objects.none()
|
queryset = ReusableMedium.objects.none()
|
||||||
permission = 'organizer.reusablemedia:read'
|
permission = 'can_manage_reusable_media'
|
||||||
write_permission = 'organizer.reusablemedia:write'
|
write_permission = 'can_manage_reusable_media'
|
||||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||||
ordering = ('-updated', '-id')
|
ordering = ('-updated', '-id')
|
||||||
ordering_fields = ('created', 'updated', 'identifier', 'type', 'id')
|
ordering_fields = ('created', 'updated', 'identifier', 'type', 'id')
|
||||||
@@ -95,8 +95,6 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
|
|||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
ctx['organizer'] = self.request.organizer
|
ctx['organizer'] = self.request.organizer
|
||||||
ctx['can_read_giftcards'] = 'organizer.giftcards:read' in self.request.orgapermset
|
|
||||||
ctx['can_read_customers'] = 'organizer.customers:read' in self.request.orgapermset
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@transaction.atomic()
|
@transaction.atomic()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user