Compare commits

..

1 Commits

Author SHA1 Message Date
Raphael Michel
b580cadc48 Organizer plugins: Do not show plugins as active if they are not on org-level 2025-08-20 13:32:36 +02:00
1189 changed files with 304299 additions and 476965 deletions

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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'

View File

@@ -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.

View File

@@ -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 \

View File

@@ -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 %}

View File

@@ -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 ' &raquo;' 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 = " &mdash; "|safe + docstitle|e %} {%- set titlesuffix = " &mdash; "|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 %}&copy; <a href="{{ path }}">Copyright</a> {{ copyright }}.{% endtrans %}
{%- else %}
{% trans copyright=copyright|e %}&copy; 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>

View File

@@ -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
--------------- ---------------

View File

@@ -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.

View File

@@ -421,94 +421,3 @@ Annulment of a check-in
: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.
:statuscode 404: The requested nonce does not exist. :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.

View File

@@ -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
===================================== ========================== ======================================================= ===================================== ========================== =======================================================

View File

@@ -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
@@ -425,6 +433,8 @@ Endpoints
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

View File

@@ -19,7 +19,6 @@ 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

View File

@@ -22,7 +22,6 @@ 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
@@ -81,12 +80,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
@@ -159,10 +163,10 @@ transmission_email_address string Optional. An em
Business customers only. Business customers only.
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
Peppol PEPPOL
"""""" """"""
The identifier ``"peppol"`` represents the transmission of XML invoices through the `Peppol`_ network. The identifier ``"peppol"`` represents the transmission of XML invoices through the `PEPPOL`_ network.
This is only available for business addresses. 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. 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: The ``transmission_info`` object may contain the following properties:
@@ -172,7 +176,7 @@ The ``transmission_info`` object may contain the following properties:
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
Field Type Description Field Type Description
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
transmission_peppol_participant_id string Required. The Peppol participant ID of the recipient. transmission_peppol_participant_id string Required. The PEPPOL participant ID of the recipient.
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
Italian Exchange System Italian Exchange System
@@ -234,7 +238,6 @@ 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":"",
@@ -271,8 +274,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",
@@ -383,7 +384,6 @@ 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":"",
@@ -420,8 +420,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",
@@ -607,5 +605,5 @@ but in other cases transmission may need to be triggered manually.
:statuscode 409: The invoice is currently in transmission :statuscode 409: The invoice is currently in transmission
.. _Peppol: https://en.wikipedia.org/wiki/PEPPOL .. _PEPPOL: https://en.wikipedia.org/wiki/PEPPOL
.. _Sistema di Interscambio: https://it.wikipedia.org/wiki/Fattura_elettronica_in_Italia .. _Sistema di Interscambio: https://it.wikipedia.org/wiki/Fattura_elettronica_in_Italia

View File

@@ -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.

View File

@@ -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

View File

@@ -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.
@@ -117,8 +116,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.
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
@@ -154,14 +151,6 @@ use_gift_cards list of strings List of unique
The ``invoice_address.transmission_type`` and ``invoice_address.transmission_info`` attributes have been added. 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 +358,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,
@@ -430,7 +418,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 +601,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,
@@ -676,7 +662,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 +978,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 +1009,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,8 +1028,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_type`` (optional, defaults to ``email``)
* ``transmission_info`` (optional, see also :ref:`rest-transmission-types`) * ``transmission_info`` (optional, see also :ref:`rest-transmission-types`)
@@ -1070,7 +1056,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 +1084,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 +1632,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 +1707,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 +1760,6 @@ Fetching individual positions
"seat": null, "seat": null,
"checkins": [ "checkins": [
{ {
"id": 1337,
"list": 44, "list": 44,
"type": "entry", "type": "entry",
"gate": null, "gate": null,
@@ -2579,7 +2504,6 @@ Order payment endpoints
{ {
"amount": "23.00", "amount": "23.00",
"comment": "Overpayment",
"mark_canceled": false "mark_canceled": false
} }

View File

@@ -110,6 +110,8 @@ Endpoints
Updates an organizer. Currently only the ``plugins`` field may be updated. Updates an organizer. Currently only the ``plugins`` field may be updated.
Permission required: "Can change organizer settings"
**Example request**: **Example request**:
.. sourcecode:: http .. sourcecode:: http
@@ -170,6 +172,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

View File

@@ -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,

View File

@@ -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

View File

@@ -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,
... ...
} }

View File

@@ -60,9 +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.added``
* ``pretix.voucher.changed`` * ``pretix.voucher.changed``
* ``pretix.voucher.deleted`` * ``pretix.voucher.deleted``

View File

@@ -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
========== ========== ============================= ============================== =============

View File

@@ -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'):
... ...

View File

@@ -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

View File

@@ -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,7 +23,7 @@ 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
""""""""" """""""""
@@ -38,7 +37,7 @@ 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

View File

@@ -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")

View File

@@ -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/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -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

View File

@@ -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.*

View File

@@ -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.*

View File

@@ -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.17.*",
"defusedcsv>=1.1.0", "defusedcsv>=1.1.0",
"dnspython==2.*", "Django[argon2]==4.2.*,>=4.2.15",
"Django[argon2]==5.2.*", "django-bootstrap3==25.2",
"django-bootstrap3==26.1", "django-compressor==4.5.1",
"django-compressor==4.6.0", "django-countries==7.6.*",
"django-countries==8.2.*",
"django-filter==25.1", "django-filter==25.1",
"django-formset-js-improved==0.5.0.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==2.0.*",
"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.3.*",
"pretix-plugin-build", "pretix-plugin-build",
"protobuf==7.34.*", "protobuf==6.32.*",
"psycopg2-binary", "psycopg2-binary",
"pycountry", "pycountry",
"pycparser==3.0", "pycparser==2.22",
"pycryptodome==3.23.*", "pycryptodome==3.23.*",
"pypdf==6.5.*", "pypdf==6.0.*",
"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.4.*",
"reportlab==4.4.*", "reportlab==4.4.*",
"requests==2.32.*", "requests==2.31.*",
"sentry-sdk==2.57.*", "sentry-sdk==2.35.*",
"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.31.*",
"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.8.*",
"pytest==9.0.*", "pytest==8.4.*",
"responses", "responses",
] ]

View File

@@ -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

View File

@@ -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.

View File

@@ -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.8.0.dev0"

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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),
),
]

View File

@@ -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)

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.
@@ -300,7 +300,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 +445,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 +561,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]
@@ -707,10 +707,7 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
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',
@@ -798,7 +795,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 +805,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 +819,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 +828,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 +940,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 +950,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',
@@ -1083,16 +1073,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):

View File

@@ -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.
@@ -27,9 +27,7 @@ from rest_framework.exceptions import ValidationError
from pretix.api.serializers.forms import form_field_to_serializer_field 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 SerializerDateFrameField from pretix.base.timeframes import SerializerDateFrameField
@@ -56,29 +54,20 @@ class ExporterSerializer(serializers.Serializer):
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) self.fields[k] = form_field_to_serializer_field(v)
def to_representation(self, instance):
# 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 instance and isinstance(instance["events"], list):
instance["events"] = [e for e in self.ex.events.filter(pk__in=instance["events"])]
instance = super().to_representation(instance)
return instance
def to_internal_value(self, data): def to_internal_value(self, data):
if isinstance(data, QueryDict): if isinstance(data, QueryDict):
data = data.copy() data = data.copy()
@@ -106,14 +95,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 +131,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:

View File

@@ -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.

View File

@@ -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.
@@ -65,9 +65,8 @@ def form_field_to_serializer_field(field):
if isinstance(field, m_from): if isinstance(field, m_from):
return m_to( return m_to(
required=field.required, required=field.required,
allow_null=not field.required and not isinstance(field, forms.BooleanField), allow_null=not field.required,
validators=field.validators, validators=field.validators,
initial=field.initial,
**{kwarg: getattr(field, kwarg, None) for kwarg in m_kwargs} **{kwarg: getattr(field, kwarg, None) for kwarg in m_kwargs}
) )

View File

@@ -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.

View File

@@ -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)

View File

@@ -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(

View File

@@ -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
@@ -53,27 +52,22 @@ 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.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, Transaction,
) )
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
@@ -193,7 +187,7 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
{"transmission_info": {r: "This field is required for the selected type of invoice transmission."}} {"transmission_info": {r: "This field is required for the selected type of invoice transmission."}}
) )
break # do not call else branch of for loop break # do not call else branch of for loop
elif t.is_exclusive(self.context["request"].event, data.get("country"), data.get("is_business")): elif t.exclusive:
if t.is_available(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({ raise ValidationError({
"transmission_type": "The transmission type '%s' must be used for this country or address type." % ( "transmission_type": "The transmission type '%s' must be used for this country or address type." % (
@@ -331,18 +325,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 +334,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 +560,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 +594,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 +617,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 +685,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
@@ -872,15 +833,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):
@@ -1045,7 +1005,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)
@@ -1141,10 +1101,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
@@ -1199,14 +1155,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()
@@ -1217,7 +1170,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:
@@ -1226,18 +1179,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.')
@@ -1312,14 +1253,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")
@@ -1634,22 +1567,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:
@@ -1763,32 +1693,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()
@@ -1804,45 +1709,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()
@@ -1891,14 +1757,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):
@@ -1912,7 +1776,7 @@ 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_is_business', 'invoice_to_company', 'invoice_to_name', 'invoice_to_street',
'invoice_to_zipcode', 'invoice_to_city', 'invoice_to_state', 'invoice_to_country', 'invoice_to_vat_id', 'invoice_to_zipcode', 'invoice_to_city', 'invoice_to_state', 'invoice_to_country', 'invoice_to_vat_id',
'invoice_to_beneficiary', 'invoice_to_transmission_info', 'custom_field', 'date', 'refers', 'locale', 'invoice_to_beneficiary', 'invoice_to_transmission_info', 'custom_field', 'date', 'refers', 'locale',

View File

@@ -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,11 @@ 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) 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'),
@@ -98,7 +98,7 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
) )
if self.context.get('commit', True): if self.context.get('commit', True):
ocm.commit(check_quotas=check_quotas) ocm.commit(check_quotas=check_quotas)
return 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 +131,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 +146,7 @@ class OrderFeeCreateForExistingOrderSerializer(OrderFeeCreateSerializer):
ocm.add_fee(f) ocm.add_fee(f)
if self.context.get('commit', True): if self.context.get('commit', True):
ocm.commit() ocm.commit()
return 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,7 +310,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) check_quotas = self.context.get('check_quotas', True)
current_seat = {'seat_guid': instance.seat.seat_guid} if instance.seat else None current_seat = {'seat_guid': instance.seat.seat_guid} if instance.seat else None
item = validated_data.get('item', instance.item) item = validated_data.get('item', instance.item)
@@ -399,7 +399,7 @@ class OrderFeeChangeSerializer(serializers.ModelSerializer):
) )
def update(self, instance, validated_data): def update(self, instance, validated_data):
ocm: OrderChangeManager = self.context['ocm'] ocm = self.context['ocm']
value = validated_data.get('value', instance.value) value = validated_data.get('value', instance.value)
try: try:

View File

@@ -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.
@@ -45,19 +45,12 @@ 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 (
get_all_event_permission_groups, get_all_organizer_permission_groups,
)
from pretix.base.plugins import ( from pretix.base.plugins import (
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID, PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_ORGANIZER,
) )
from pretix.base.services.mail import mail from pretix.base.services.mail import SendMailException, 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
@@ -313,128 +306,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 +339,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 +353,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 +363,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 +439,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 +484,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):

View File

@@ -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):

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.
@@ -67,7 +67,6 @@ 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'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,7 +83,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'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')
@@ -93,7 +92,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 +111,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)

View File

@@ -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.

View File

@@ -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(

View File

@@ -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.
@@ -56,8 +56,7 @@ from pretix.api.serializers.checkin import (
) )
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,7 +66,6 @@ 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,
) )
@@ -98,16 +96,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 +107,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 +177,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 +370,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 +393,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 +446,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,8 +811,8 @@ 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()
@@ -872,7 +843,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
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()
@@ -921,9 +892,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:
@@ -984,16 +955,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 +980,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:
@@ -1039,9 +1009,9 @@ class CheckinRPCSearchView(ListAPIView):
class CheckinRPCAnnulView(views.APIView): class CheckinRPCAnnulView(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:
@@ -1110,25 +1080,3 @@ class CheckinRPCAnnulView(views.APIView):
checkin_annulled.send(ci.position.order.event, checkin=ci) checkin_annulled.send(ci.position.order.event, checkin=ci)
return Response({"status": "ok"}, status=status.HTTP_200_OK) 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().select_related(
"position",
"device",
)
return qs
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
return ctx

View File

@@ -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.

View File

@@ -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(

View File

@@ -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()
@@ -603,7 +589,7 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
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 +636,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 +662,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 +674,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 +701,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

View File

@@ -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

View File

@@ -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.

View File

@@ -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()

View File

@@ -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()

View File

@@ -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.

View File

@@ -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,10 +57,9 @@ from pretix.api.serializers.order import (
BlockedTicketSecretSerializer, InvoiceSerializer, OrderCreateSerializer, BlockedTicketSecretSerializer, InvoiceSerializer, OrderCreateSerializer,
OrderPaymentCreateSerializer, OrderPaymentSerializer, OrderPaymentCreateSerializer, OrderPaymentSerializer,
OrderPositionSerializer, OrderRefundCreateSerializer, OrderPositionSerializer, OrderRefundCreateSerializer,
OrderRefundSerializer, OrderSerializer, OrganizerOrderPositionSerializer, OrderRefundSerializer, OrderSerializer, OrganizerTransactionSerializer,
OrganizerTransactionSerializer, PriceCalcSerializer, PrintLogSerializer, PriceCalcSerializer, PrintLogSerializer, RevokedTicketSecretSerializer,
RevokedTicketSecretSerializer, SimulatedOrderSerializer, SimulatedOrderSerializer, TransactionSerializer,
TransactionSerializer,
) )
from pretix.api.serializers.orderchange import ( from pretix.api.serializers.orderchange import (
BlockNameSerializer, OrderChangeOperationSerializer, BlockNameSerializer, OrderChangeOperationSerializer,
@@ -91,6 +90,7 @@ from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified, generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
regenerate_invoice, transmit_invoice, regenerate_invoice, transmit_invoice,
) )
from pretix.base.services.mail import SendMailException
from pretix.base.services.orders import ( from pretix.base.services.orders import (
OrderChangeManager, OrderError, _order_placed_email, OrderChangeManager, OrderError, _order_placed_email,
_order_placed_email_attendee, approve_order, cancel_order, deny_order, _order_placed_email_attendee, approve_order, cancel_order, deny_order,
@@ -317,7 +317,7 @@ class OrderViewSetMixin:
class OrganizerOrderViewSet(OrderViewSetMixin, viewsets.ReadOnlyModelViewSet): class OrganizerOrderViewSet(OrderViewSetMixin, viewsets.ReadOnlyModelViewSet):
def get_base_queryset(self): def get_base_queryset(self):
perm = "event.orders:read" if self.request.method in SAFE_METHODS else "event.orders:write" perm = "can_view_orders" if self.request.method in SAFE_METHODS else "can_change_orders"
if isinstance(self.request.auth, (TeamAPIToken, Device)): if isinstance(self.request.auth, (TeamAPIToken, Device)):
return Order.objects.filter( return Order.objects.filter(
event__organizer=self.request.organizer, event__organizer=self.request.organizer,
@@ -338,13 +338,12 @@ class OrganizerOrderViewSet(OrderViewSetMixin, viewsets.ReadOnlyModelViewSet):
class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet): class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
permission = 'event.orders:read' permission = 'can_view_orders'
write_permission = 'event.orders:write' write_permission = 'can_change_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['auth'] = self.request.auth
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true' ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
return ctx return ctx
@@ -439,6 +438,8 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except PaymentException as e: except PaymentException as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except SendMailException:
pass
return self.retrieve(request, [], **kwargs) return self.retrieve(request, [], **kwargs)
return Response( return Response(
@@ -632,7 +633,10 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
order = self.get_object() order = self.get_object()
if not order.email: if not order.email:
return Response({'detail': 'There is no email address associated with this order.'}, status=status.HTTP_400_BAD_REQUEST) return Response({'detail': 'There is no email address associated with this order.'}, status=status.HTTP_400_BAD_REQUEST)
order.resend_link(user=self.request.user, auth=self.request.auth) try:
order.resend_link(user=self.request.user, auth=self.request.auth)
except SendMailException:
return Response({'detail': _('There was an error sending the mail. Please try again later.')}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
return Response( return Response(
status=status.HTTP_204_NO_CONTENT status=status.HTTP_204_NO_CONTENT
@@ -739,7 +743,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
user=request.user if request.user.is_authenticated else None, user=request.user if request.user.is_authenticated else None,
auth=request.auth, auth=request.auth,
) )
order_placed.send(self.request.event, order=order, bulk=False) order_placed.send(self.request.event, order=order)
if order.status == Order.STATUS_PAID: if order.status == Order.STATUS_PAID:
order_paid.send(self.request.event, order=order) order_paid.send(self.request.event, order=order)
order.log_action( order.log_action(
@@ -760,13 +764,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
) and not order.invoices.last() ) and not order.invoices.last()
invoice = None invoice = None
if gen_invoice: if gen_invoice:
try: invoice = generate_invoice(order, trigger_pdf=True)
invoice = generate_invoice(order, trigger_pdf=True)
except Exception as e:
logger.exception("Could not generate invoice.")
order.log_action("pretix.event.order.invoice.failed", data={
"exception": str(e)
})
# Refresh serializer only after running signals # Refresh serializer only after running signals
prefetch_related_objects([order], self._positions_prefetch(request)) prefetch_related_objects([order], self._positions_prefetch(request))
@@ -1066,12 +1064,15 @@ with scopes_disabled():
} }
class OrderPositionViewSetMixin: class OrderPositionViewSet(viewsets.ModelViewSet):
serializer_class = OrderPositionSerializer
queryset = OrderPosition.all.none() queryset = OrderPosition.all.none()
filter_backends = (DjangoFilterBackend, RichOrderingFilter) filter_backends = (DjangoFilterBackend, RichOrderingFilter)
ordering = ('order__datetime', 'positionid') ordering = ('order__datetime', 'positionid')
ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',) ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',)
filterset_class = OrderPositionFilter filterset_class = OrderPositionFilter
permission = 'can_view_orders'
write_permission = 'can_change_orders'
ordering_custom = { ordering_custom = {
'attendee_name': { 'attendee_name': {
'_order': F('display_name').asc(nulls_first=True), '_order': F('display_name').asc(nulls_first=True),
@@ -1085,7 +1086,8 @@ class OrderPositionViewSetMixin:
def get_serializer_context(self): def get_serializer_context(self):
ctx = super().get_serializer_context() ctx = super().get_serializer_context()
ctx['pdf_data'] = False ctx['event'] = self.request.event
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
ctx['check_quotas'] = self.request.query_params.get('check_quotas', 'true').lower() == 'true' ctx['check_quotas'] = self.request.query_params.get('check_quotas', 'true').lower() == 'true'
return ctx return ctx
@@ -1094,8 +1096,9 @@ class OrderPositionViewSetMixin:
qs = OrderPosition.all qs = OrderPosition.all
else: else:
qs = OrderPosition.objects qs = OrderPosition.objects
qs = qs.filter(order__event__organizer=self.request.organizer)
if self.request.query_params.get('pdf_data', 'false').lower() == 'true' and getattr(self.request, 'event', None): qs = qs.filter(order__event=self.request.event)
if self.request.query_params.get('pdf_data', 'false').lower() == 'true':
prefetch_related_objects([self.request.organizer], 'meta_properties') prefetch_related_objects([self.request.organizer], 'meta_properties')
prefetch_related_objects( prefetch_related_objects(
[self.request.event], [self.request.event],
@@ -1150,9 +1153,9 @@ class OrderPositionViewSetMixin:
qs = qs.prefetch_related( qs = qs.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related("device")), Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')), Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question', 'order__event', 'order__event__organizer' 'answers', 'answers__options', 'answers__question',
).select_related( ).select_related(
'item', 'order', 'seat' 'item', 'order', 'order__event', 'order__event__organizer', 'seat'
) )
return qs return qs
@@ -1164,49 +1167,6 @@ class OrderPositionViewSetMixin:
return prov return prov
raise NotFound('Unknown output provider.') raise NotFound('Unknown output provider.')
class OrganizerOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrganizerOrderPositionSerializer
permission = None
write_permission = None
def get_queryset(self):
qs = super().get_queryset()
perm = "event.orders:read" if self.request.method in SAFE_METHODS else "event.orders:write"
if isinstance(self.request.auth, (TeamAPIToken, Device)):
auth_obj = self.request.auth
elif self.request.user.is_authenticated:
auth_obj = self.request.user
else:
raise PermissionDenied("Unknown authentication scheme")
qs = qs.filter(
order__event__in=auth_obj.get_events_with_permission(perm, request=self.request).filter(
organizer=self.request.organizer
)
)
return qs
class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet):
serializer_class = OrderPositionSerializer
permission = 'event.orders:read'
write_permission = 'event.orders:write'
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
return ctx
def get_queryset(self):
qs = super().get_queryset()
qs = qs.filter(order__event=self.request.event)
return qs
@action(detail=True, methods=['POST'], url_name='price_calc') @action(detail=True, methods=['POST'], url_name='price_calc')
def price_calc(self, request, *args, **kwargs): def price_calc(self, request, *args, **kwargs):
""" """
@@ -1613,8 +1573,8 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet): class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrderPaymentSerializer serializer_class = OrderPaymentSerializer
queryset = OrderPayment.objects.none() queryset = OrderPayment.objects.none()
permission = 'event.orders:read' permission = 'can_view_orders'
write_permission = 'event.orders:write' write_permission = 'can_change_orders'
lookup_field = 'local_id' lookup_field = 'local_id'
def get_serializer_context(self): def get_serializer_context(self):
@@ -1649,6 +1609,8 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
) )
except Quota.QuotaExceededException: except Quota.QuotaExceededException:
pass pass
except SendMailException:
pass
serializer = OrderPaymentSerializer(r, context=serializer.context) serializer = OrderPaymentSerializer(r, context=serializer.context)
@@ -1686,6 +1648,8 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except PaymentException as e: except PaymentException as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except SendMailException:
pass
return self.retrieve(request, [], **kwargs) return self.retrieve(request, [], **kwargs)
@action(detail=True, methods=['POST']) @action(detail=True, methods=['POST'])
@@ -1699,9 +1663,6 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
else: else:
mark_refunded = request.data.get('mark_canceled', False) mark_refunded = request.data.get('mark_canceled', False)
if not isinstance(request.data.get("comment", ""), str):
return Response({'comment': 'Invalid type.'}, status=status.HTTP_400_BAD_REQUEST)
if payment.state != OrderPayment.PAYMENT_STATE_CONFIRMED: if payment.state != OrderPayment.PAYMENT_STATE_CONFIRMED:
return Response({'detail': 'Invalid state of payment.'}, status=status.HTTP_400_BAD_REQUEST) return Response({'detail': 'Invalid state of payment.'}, status=status.HTTP_400_BAD_REQUEST)
@@ -1728,7 +1689,6 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
amount=amount, amount=amount,
provider=payment.provider, provider=payment.provider,
info='{}', info='{}',
comment=request.data.get("comment"),
) )
payment.order.log_action('pretix.event.order.refund.created', { payment.order.log_action('pretix.event.order.refund.created', {
'local_id': r.local_id, 'local_id': r.local_id,
@@ -1786,8 +1746,8 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet): class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrderRefundSerializer serializer_class = OrderRefundSerializer
queryset = OrderRefund.objects.none() queryset = OrderRefund.objects.none()
permission = 'event.orders:read' permission = 'can_view_orders'
write_permission = 'event.orders:write' write_permission = 'can_change_orders'
lookup_field = 'local_id' lookup_field = 'local_id'
def get_queryset(self): def get_queryset(self):
@@ -1944,18 +1904,13 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
ordering = ('nr',) ordering = ('nr',)
ordering_fields = ('nr', 'date') ordering_fields = ('nr', 'date')
filterset_class = InvoiceFilter filterset_class = InvoiceFilter
permission = 'can_view_orders'
lookup_url_kwarg = 'number' lookup_url_kwarg = 'number'
lookup_field = 'nr' lookup_field = 'nr'
write_permission = 'can_change_orders'
def _get_permission_name(self, request):
if 'event' in request.resolver_match.kwargs:
if request.method not in SAFE_METHODS:
return "event.orders:write"
return "event.orders:read"
return None # org-level is handled by event__in check
def get_queryset(self): def get_queryset(self):
perm = "event.orders:read" if self.request.method in SAFE_METHODS else "event.orders:write" perm = "can_view_orders" if self.request.method in SAFE_METHODS else "can_change_orders"
if getattr(self.request, 'event', None): if getattr(self.request, 'event', None):
qs = self.request.event.invoices qs = self.request.event.invoices
elif isinstance(self.request.auth, (TeamAPIToken, Device)): elif isinstance(self.request.auth, (TeamAPIToken, Device)):
@@ -2065,7 +2020,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
else: else:
order = Order.objects.select_for_update(of=OF_SELF).get(pk=inv.order_id) order = Order.objects.select_for_update(of=OF_SELF).get(pk=inv.order_id)
c = generate_cancellation(inv) c = generate_cancellation(inv)
if invoice_qualified(order): if inv.order.status != Order.STATUS_CANCELED:
inv = generate_invoice(order) inv = generate_invoice(order)
else: else:
inv = c inv = c
@@ -2096,8 +2051,8 @@ class RevokedSecretViewSet(viewsets.ReadOnlyModelViewSet):
ordering = ('-created',) ordering = ('-created',)
ordering_fields = ('created', 'secret') ordering_fields = ('created', 'secret')
filterset_class = RevokedSecretFilter filterset_class = RevokedSecretFilter
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 RevokedTicketSecret.objects.filter(event=self.request.event) return RevokedTicketSecret.objects.filter(event=self.request.event)
@@ -2118,8 +2073,8 @@ class BlockedSecretViewSet(viewsets.ReadOnlyModelViewSet):
filter_backends = (DjangoFilterBackend, TotalOrderingFilter) filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
ordering = ('-updated', '-pk') ordering = ('-updated', '-pk')
filterset_class = BlockedSecretFilter filterset_class = BlockedSecretFilter
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 BlockedTicketSecret.objects.filter(event=self.request.event) return BlockedTicketSecret.objects.filter(event=self.request.event)
@@ -2154,7 +2109,7 @@ class TransactionViewSet(viewsets.ReadOnlyModelViewSet):
ordering = ('datetime', 'pk') ordering = ('datetime', 'pk')
ordering_fields = ('datetime', 'created', 'id',) ordering_fields = ('datetime', 'created', 'id',)
filterset_class = TransactionFilter filterset_class = TransactionFilter
permission = 'event.orders:read' permission = 'can_view_orders'
def get_queryset(self): def get_queryset(self):
return Transaction.objects.filter(order__event=self.request.event).select_related("order") return Transaction.objects.filter(order__event=self.request.event).select_related("order")
@@ -2171,11 +2126,11 @@ class OrganizerTransactionViewSet(TransactionViewSet):
if isinstance(self.request.auth, (TeamAPIToken, Device)): if isinstance(self.request.auth, (TeamAPIToken, Device)):
qs = qs.filter( qs = qs.filter(
order__event__in=self.request.auth.get_events_with_permission("event.orders:read"), order__event__in=self.request.auth.get_events_with_permission("can_view_orders"),
) )
elif self.request.user.is_authenticated: elif self.request.user.is_authenticated:
qs = qs.filter( qs = qs.filter(
order__event__in=self.request.user.get_events_with_permission("event.orders:read", request=self.request) order__event__in=self.request.user.get_events_with_permission("can_view_orders", request=self.request)
) )
else: else:
raise PermissionDenied("Unknown authentication scheme") raise PermissionDenied("Unknown authentication scheme")

View File

@@ -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.
@@ -70,7 +70,7 @@ class OrganizerViewSet(mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
filter_backends = (TotalOrderingFilter,) filter_backends = (TotalOrderingFilter,)
ordering = ('slug',) ordering = ('slug',)
ordering_fields = ('name', 'slug') ordering_fields = ('name', 'slug')
write_permission = "organizer.settings.general:write" write_permission = "can_change_organizer_settings"
def get_queryset(self): def get_queryset(self):
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
@@ -154,8 +154,8 @@ class OrganizerViewSet(mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
class SeatingPlanViewSet(viewsets.ModelViewSet): class SeatingPlanViewSet(viewsets.ModelViewSet):
serializer_class = SeatingPlanSerializer serializer_class = SeatingPlanSerializer
queryset = SeatingPlan.objects.none() queryset = SeatingPlan.objects.none()
permission = None permission = 'can_change_organizer_settings'
write_permission = 'organizer.seatingplans:write' write_permission = 'can_change_organizer_settings'
def get_queryset(self): def get_queryset(self):
return self.request.organizer.seating_plans.order_by('name') return self.request.organizer.seating_plans.order_by('name')
@@ -221,8 +221,8 @@ with scopes_disabled():
class GiftCardViewSet(viewsets.ModelViewSet): class GiftCardViewSet(viewsets.ModelViewSet):
serializer_class = GiftCardSerializer serializer_class = GiftCardSerializer
queryset = GiftCard.objects.none() queryset = GiftCard.objects.none()
permission = 'organizer.giftcards:read' permission = 'can_manage_gift_cards'
write_permission = 'organizer.giftcards:write' write_permission = 'can_manage_gift_cards'
filter_backends = (DjangoFilterBackend,) filter_backends = (DjangoFilterBackend,)
filterset_class = GiftCardFilter filterset_class = GiftCardFilter
@@ -249,24 +249,12 @@ class GiftCardViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer): def perform_create(self, serializer):
value = serializer.validated_data.pop('value') value = serializer.validated_data.pop('value')
inst = serializer.save(issuer=self.request.organizer) inst = serializer.save(issuer=self.request.organizer)
inst.log_action(
action='pretix.giftcards.created',
user=self.request.user,
auth=self.request.auth,
)
inst.transactions.create(value=value, acceptor=self.request.organizer) inst.transactions.create(value=value, acceptor=self.request.organizer)
inst.log_action( inst.log_action(
action='pretix.giftcards.transaction.manual', 'pretix.giftcards.transaction.manual',
user=self.request.user, user=self.request.user,
auth=self.request.auth, auth=self.request.auth,
data=merge_dicts( data=merge_dicts(self.request.data, {'id': inst.pk})
self.request.data,
{
'id': inst.pk,
'acceptor_id': self.request.organizer.id,
'acceptor_slug': self.request.organizer.slug
}
)
) )
@transaction.atomic() @transaction.atomic()
@@ -281,7 +269,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency, inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency,
testmode=serializer.instance.testmode) testmode=serializer.instance.testmode)
inst.log_action( inst.log_action(
action='pretix.giftcards.modified', 'pretix.giftcards.modified',
user=self.request.user, user=self.request.user,
auth=self.request.auth, auth=self.request.auth,
data=self.request.data, data=self.request.data,
@@ -294,14 +282,10 @@ class GiftCardViewSet(viewsets.ModelViewSet):
diff = value - old_value diff = value - old_value
inst.transactions.create(value=diff, acceptor=self.request.organizer) inst.transactions.create(value=diff, acceptor=self.request.organizer)
inst.log_action( inst.log_action(
action='pretix.giftcards.transaction.manual', 'pretix.giftcards.transaction.manual',
user=self.request.user, user=self.request.user,
auth=self.request.auth, auth=self.request.auth,
data={ data={'value': diff}
'value': diff,
'acceptor_id': self.request.organizer.id,
'acceptor_slug': self.request.organizer.slug
}
) )
return inst return inst
@@ -325,15 +309,10 @@ class GiftCardViewSet(viewsets.ModelViewSet):
}, status=status.HTTP_409_CONFLICT) }, status=status.HTTP_409_CONFLICT)
gc.transactions.create(value=value, text=text, info=info, acceptor=self.request.organizer) gc.transactions.create(value=value, text=text, info=info, acceptor=self.request.organizer)
gc.log_action( gc.log_action(
action='pretix.giftcards.transaction.manual', 'pretix.giftcards.transaction.manual',
user=self.request.user, user=self.request.user,
auth=self.request.auth, auth=self.request.auth,
data={ data={'value': value, 'text': text}
'value': value,
'text': text,
'acceptor_id': self.request.organizer.id,
'acceptor_slug': self.request.organizer.slug
}
) )
return Response(GiftCardSerializer(gc, context=self.get_serializer_context()).data, status=status.HTTP_200_OK) return Response(GiftCardSerializer(gc, context=self.get_serializer_context()).data, status=status.HTTP_200_OK)
@@ -344,8 +323,8 @@ class GiftCardViewSet(viewsets.ModelViewSet):
class GiftCardTransactionViewSet(viewsets.ReadOnlyModelViewSet): class GiftCardTransactionViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = GiftCardTransactionSerializer serializer_class = GiftCardTransactionSerializer
queryset = GiftCardTransaction.objects.none() queryset = GiftCardTransaction.objects.none()
permission = 'organizer.giftcards:read' permission = 'can_manage_gift_cards'
write_permission = 'organizer.giftcards:write' write_permission = 'can_manage_gift_cards'
@cached_property @cached_property
def giftcard(self): def giftcard(self):
@@ -362,8 +341,8 @@ class GiftCardTransactionViewSet(viewsets.ReadOnlyModelViewSet):
class TeamViewSet(viewsets.ModelViewSet): class TeamViewSet(viewsets.ModelViewSet):
serializer_class = TeamSerializer serializer_class = TeamSerializer
queryset = Team.objects.none() queryset = Team.objects.none()
permission = 'organizer.teams:write' permission = 'can_change_teams'
write_permission = 'organizer.teams:write' write_permission = 'can_change_teams'
def get_queryset(self): def get_queryset(self):
return self.request.organizer.teams.order_by('pk') return self.request.organizer.teams.order_by('pk')
@@ -402,8 +381,8 @@ class TeamViewSet(viewsets.ModelViewSet):
class TeamMemberViewSet(DestroyModelMixin, viewsets.ReadOnlyModelViewSet): class TeamMemberViewSet(DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = TeamMemberSerializer serializer_class = TeamMemberSerializer
queryset = User.objects.none() queryset = User.objects.none()
permission = 'organizer.teams:write' permission = 'can_change_teams'
write_permission = 'organizer.teams:write' write_permission = 'can_change_teams'
@cached_property @cached_property
def team(self): def team(self):
@@ -431,8 +410,8 @@ class TeamMemberViewSet(DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
class TeamInviteViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet): class TeamInviteViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = TeamInviteSerializer serializer_class = TeamInviteSerializer
queryset = TeamInvite.objects.none() queryset = TeamInvite.objects.none()
permission = 'organizer.teams:write' permission = 'can_change_teams'
write_permission = 'organizer.teams:write' write_permission = 'can_change_teams'
@cached_property @cached_property
def team(self): def team(self):
@@ -468,8 +447,8 @@ class TeamInviteViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyMo
class TeamAPITokenViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet): class TeamAPITokenViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = TeamAPITokenSerializer serializer_class = TeamAPITokenSerializer
queryset = TeamAPIToken.objects.none() queryset = TeamAPIToken.objects.none()
permission = 'organizer.teams:write' permission = 'can_change_teams'
write_permission = 'organizer.teams:write' write_permission = 'can_change_teams'
@cached_property @cached_property
def team(self): def team(self):
@@ -532,8 +511,8 @@ class DeviceViewSet(mixins.CreateModelMixin,
GenericViewSet): GenericViewSet):
serializer_class = DeviceSerializer serializer_class = DeviceSerializer
queryset = Device.objects.none() queryset = Device.objects.none()
permission = 'organizer.devices:read' permission = 'can_change_organizer_settings'
write_permission = 'organizer.devices:write' write_permission = 'can_change_organizer_settings'
lookup_field = 'device_id' lookup_field = 'device_id'
def get_queryset(self): def get_queryset(self):
@@ -542,9 +521,6 @@ class DeviceViewSet(mixins.CreateModelMixin,
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_see_tokens'] = (
self.request.user if self.request.user and self.request.user.is_authenticated else self.request.auth
).has_organizer_permission(self.request.organizer, 'organizer.devices:write', request=self.request)
return ctx return ctx
@transaction.atomic() @transaction.atomic()
@@ -570,12 +546,11 @@ class DeviceViewSet(mixins.CreateModelMixin,
class OrganizerSettingsView(views.APIView): class OrganizerSettingsView(views.APIView):
permission = None permission = 'can_change_organizer_settings'
write_permission = 'organizer.settings.general:write'
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={ s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
'request': request, 'permissions': request.orgapermset 'request': request
}) })
if 'explain' in request.GET: if 'explain' in request.GET:
return Response({ return Response({
@@ -592,7 +567,7 @@ class OrganizerSettingsView(views.APIView):
s = OrganizerSettingsSerializer( s = OrganizerSettingsSerializer(
instance=request.organizer.settings, data=request.data, partial=True, instance=request.organizer.settings, data=request.data, partial=True,
organizer=request.organizer, context={ organizer=request.organizer, context={
'request': request, 'permissions': request.orgapermset 'request': request
} }
) )
s.is_valid(raise_exception=True) s.is_valid(raise_exception=True)
@@ -604,7 +579,7 @@ class OrganizerSettingsView(views.APIView):
} }
) )
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={ s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
'request': request, 'permissions': request.orgapermset 'request': request
}) })
return Response(s.data) return Response(s.data)
@@ -621,8 +596,7 @@ with scopes_disabled():
class CustomerViewSet(viewsets.ModelViewSet): class CustomerViewSet(viewsets.ModelViewSet):
serializer_class = CustomerSerializer serializer_class = CustomerSerializer
queryset = Customer.objects.none() queryset = Customer.objects.none()
permission = 'organizer.customers:read' permission = 'can_manage_customers'
write_permission = 'organizer.customers:write'
lookup_field = 'identifier' lookup_field = 'identifier'
filter_backends = (DjangoFilterBackend,) filter_backends = (DjangoFilterBackend,)
filterset_class = CustomerFilter filterset_class = CustomerFilter
@@ -682,7 +656,7 @@ class CustomerViewSet(viewsets.ModelViewSet):
class MembershipTypeViewSet(viewsets.ModelViewSet): class MembershipTypeViewSet(viewsets.ModelViewSet):
serializer_class = MembershipTypeSerializer serializer_class = MembershipTypeSerializer
queryset = MembershipType.objects.none() queryset = MembershipType.objects.none()
permission = 'organizer.settings.general:write' permission = 'can_change_organizer_settings'
def get_queryset(self): def get_queryset(self):
qs = self.request.organizer.membership_types.all() qs = self.request.organizer.membership_types.all()
@@ -739,15 +713,14 @@ with scopes_disabled():
class MembershipViewSet(viewsets.ModelViewSet): class MembershipViewSet(viewsets.ModelViewSet):
serializer_class = MembershipSerializer serializer_class = MembershipSerializer
queryset = Membership.objects.none() queryset = Membership.objects.none()
permission = 'organizer.customers:read' permission = 'can_manage_customers'
write_permission = 'organizer.customers:write'
filter_backends = (DjangoFilterBackend,) filter_backends = (DjangoFilterBackend,)
filterset_class = MembershipFilter filterset_class = MembershipFilter
def get_queryset(self): def get_queryset(self):
return Membership.objects.filter( return Membership.objects.filter(
customer__organizer=self.request.organizer customer__organizer=self.request.organizer
).select_related('customer') )
def get_serializer_context(self): def get_serializer_context(self):
ctx = super().get_serializer_context() ctx = super().get_serializer_context()
@@ -790,8 +763,8 @@ with scopes_disabled():
class SalesChannelViewSet(viewsets.ModelViewSet): class SalesChannelViewSet(viewsets.ModelViewSet):
serializer_class = SalesChannelSerializer serializer_class = SalesChannelSerializer
queryset = SalesChannel.objects.none() queryset = SalesChannel.objects.none()
permission = 'organizer.settings.general:write' permission = 'can_change_organizer_settings'
write_permission = 'organizer.settings.general:write' write_permission = 'can_change_organizer_settings'
filter_backends = (DjangoFilterBackend,) filter_backends = (DjangoFilterBackend,)
filterset_class = SalesChannelFilter filterset_class = SalesChannelFilter
lookup_field = 'identifier' lookup_field = 'identifier'

View File

@@ -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.
@@ -204,7 +204,7 @@ class ShreddersMixin:
class EventShreddersViewSet(ShreddersMixin, viewsets.ViewSet): class EventShreddersViewSet(ShreddersMixin, viewsets.ViewSet):
permission = 'event.orders:write' permission = 'can_change_orders'
def get_serializer_kwargs(self): def get_serializer_kwargs(self):
return {} return {}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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/>.
# #
from django.db import transaction from django.db import transaction
from django.db.models import F, Q from django.db.models import F, Q
from django.utils.timezone import now from django.utils.timezone import now
@@ -62,16 +61,11 @@ class VoucherViewSet(viewsets.ModelViewSet):
ordering = ('id',) ordering = ('id',)
ordering_fields = ('id', 'code', 'max_usages', 'valid_until', 'value') ordering_fields = ('id', 'code', 'max_usages', 'valid_until', 'value')
filterset_class = VoucherFilter filterset_class = VoucherFilter
permission = 'event.vouchers:read' permission = 'can_view_vouchers'
write_permission = 'event.vouchers:write' write_permission = 'can_change_vouchers'
@scopes_disabled() # we have an event check here, and we can save some performance on subqueries
def get_queryset(self): def get_queryset(self):
return Voucher.annotate_budget_used( return self.request.event.vouchers.select_related('seat').all()
self.request.event.vouchers
).select_related(
'item', 'quota', 'seat', 'variation'
)
@transaction.atomic() @transaction.atomic()
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):

View File

@@ -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.
@@ -51,8 +51,8 @@ class WaitingListViewSet(viewsets.ModelViewSet):
ordering = ('created', 'pk',) ordering = ('created', 'pk',)
ordering_fields = ('id', 'created', 'email', 'item') ordering_fields = ('id', 'created', 'email', 'item')
filterset_class = WaitingListFilter filterset_class = WaitingListFilter
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 self.request.event.waitinglistentries.all() return self.request.event.waitinglistentries.all()

View File

@@ -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.
@@ -35,8 +35,8 @@ class WebhookFilter(FilterSet):
class WebHookViewSet(viewsets.ModelViewSet): class WebHookViewSet(viewsets.ModelViewSet):
serializer_class = WebHookSerializer serializer_class = WebHookSerializer
queryset = WebHook.objects.none() queryset = WebHook.objects.none()
permission = 'organizer.settings.general:write' permission = 'can_change_organizer_settings'
write_permission = 'organizer.settings.general:write' write_permission = 'can_change_organizer_settings'
filter_backends = (DjangoFilterBackend,) filter_backends = (DjangoFilterBackend,)
filterset_class = WebhookFilter filterset_class = WebhookFilter

View File

@@ -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.
@@ -43,7 +43,6 @@ from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
from pretix.base.signals import periodic_task from pretix.base.signals import periodic_task
from pretix.celery_app import app from pretix.celery_app import app
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
from pretix.helpers.celery import get_task_priority
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_ALL_EVENTS = None _ALL_EVENTS = None
@@ -174,38 +173,6 @@ class ParametrizedEventWebhookEvent(ParametrizedWebhookEvent):
} }
class ParametrizedGiftcardWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry):
giftcard = logentry.content_object
if not giftcard:
return None
return {
'notification_id': logentry.pk,
'issuer_id': logentry.organizer_id,
'issuer_slug': logentry.organizer.slug,
'giftcard': giftcard.pk,
'action': logentry.action_type,
}
class ParametrizedGiftcardTransactionWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry):
giftcard = logentry.content_object
if not giftcard:
return None
return {
'notification_id': logentry.pk,
'issuer_id': logentry.organizer_id,
'issuer_slug': logentry.organizer.slug,
'acceptor_id': logentry.parsed_data.get('acceptor_id'),
'acceptor_slug': logentry.parsed_data.get('acceptor_slug'),
'giftcard': giftcard.pk,
'action': logentry.action_type,
}
class ParametrizedVoucherWebhookEvent(ParametrizedWebhookEvent): class ParametrizedVoucherWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry): def build_payload(self, logentry: LogEntry):
@@ -465,18 +432,6 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.customer.anonymized', 'pretix.customer.anonymized',
_('Customer account anonymized'), _('Customer account anonymized'),
), ),
ParametrizedGiftcardWebhookEvent(
'pretix.giftcards.created',
_('Gift card added'),
),
ParametrizedGiftcardWebhookEvent(
'pretix.giftcards.modified',
_('Gift card modified'),
),
ParametrizedGiftcardTransactionWebhookEvent(
'pretix.giftcards.transaction.*',
_('Gift card used in transaction'),
)
) )
@@ -484,12 +439,8 @@ def register_default_webhook_events(sender, **kwargs):
def notify_webhooks(logentry_ids: list): def notify_webhooks(logentry_ids: list):
if not isinstance(logentry_ids, list): if not isinstance(logentry_ids, list):
logentry_ids = [logentry_ids] logentry_ids = [logentry_ids]
qs = LogEntry.all.select_related( qs = LogEntry.all.select_related('event', 'event__organizer', 'organizer').filter(id__in=logentry_ids)
'event', 'event__organizer', 'organizer' _org, _at, webhooks = None, None, None
).order_by(
'action_type', 'organizer_id', 'event_id',
).filter(id__in=logentry_ids)
_org, _at, _ev, webhooks = None, None, None, None
for logentry in qs: for logentry in qs:
if not logentry.organizer: if not logentry.organizer:
break # We need to know the organizer break # We need to know the organizer
@@ -499,7 +450,7 @@ def notify_webhooks(logentry_ids: list):
if not notification_type: if not notification_type:
break # Ignore, no webhooks for this event type break # Ignore, no webhooks for this event type
if _org != logentry.organizer or _at != logentry.action_type or _ev != logentry.event_id or webhooks is None: if _org != logentry.organizer or _at != logentry.action_type or webhooks is None:
_org = logentry.organizer _org = logentry.organizer
_at = logentry.action_type _at = logentry.action_type
@@ -519,10 +470,7 @@ def notify_webhooks(logentry_ids: list):
) )
for wh in webhooks: for wh in webhooks:
send_webhook.apply_async( send_webhook.apply_async(args=(logentry.id, notification_type.action_type, wh.pk))
args=(logentry.id, notification_type.action_type, wh.pk),
priority=get_task_priority("notifications", logentry.organizer_id),
)
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=60, acks_late=True, autoretry_for=(DatabaseError,),) @app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=60, acks_late=True, autoretry_for=(DatabaseError,),)

View File

@@ -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.

Some files were not shown because too many files have changed in this diff Show More