mirror of
https://github.com/pretix/pretix.git
synced 2026-04-23 23:22:32 +00:00
Compare commits
1 Commits
fix-csv-im
...
plugins-pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b580cadc48 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -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 }}
|
||||||
|
|||||||
4
.github/workflows/docs.yml
vendored
4
.github/workflows/docs.yml
vendored
@@ -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
|
||||||
|
|||||||
8
.github/workflows/strings.yml
vendored
8
.github/workflows/strings.yml
vendored
@@ -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
|
||||||
|
|||||||
12
.github/workflows/style.yml
vendored
12
.github/workflows/style.yml
vendored
@@ -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
|
||||||
|
|||||||
10
.github/workflows/tests.yml
vendored
10
.github/workflows/tests.yml
vendored
@@ -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'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
This file is part of pretix (Community Edition).
|
This file is part of pretix (Community Edition).
|
||||||
|
|
||||||
Copyright (C) 2014-2020 Raphael Michel and contributors
|
Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
Copyright (C) 2020-today pretix GmbH and contributors
|
Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
Public License as published by the Free Software Foundation in version 3 of the License.
|
Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,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 \
|
||||||
|
|||||||
174
doc/_themes/pretix_theme/layout.html
vendored
174
doc/_themes/pretix_theme/layout.html
vendored
@@ -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 %}
|
||||||
|
|||||||
340
doc/_themes/pretix_theme/layout_old.html
vendored
340
doc/_themes/pretix_theme/layout_old.html
vendored
@@ -1,86 +1,136 @@
|
|||||||
{# TEMPLATE VAR SETTINGS #}
|
{#
|
||||||
|
basic/layout.html
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Master layout template for Sphinx themes.
|
||||||
|
|
||||||
|
:copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS.
|
||||||
|
:license: BSD, see LICENSE for details.
|
||||||
|
#}
|
||||||
|
{%- block doctype -%}
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
{%- endblock %}
|
||||||
|
{%- set reldelim1 = reldelim1 is not defined and ' »' or reldelim1 %}
|
||||||
|
{%- set reldelim2 = reldelim2 is not defined and ' |' or reldelim2 %}
|
||||||
|
{%- set render_sidebar = (not embedded) and (not theme_nosidebar|tobool) and
|
||||||
|
(sidebars != []) %}
|
||||||
{%- set url_root = pathto('', 1) %}
|
{%- set url_root = pathto('', 1) %}
|
||||||
|
{# XXX necessary? #}
|
||||||
{%- if url_root == '#' %}{% set url_root = '' %}{% endif %}
|
{%- if url_root == '#' %}{% set url_root = '' %}{% endif %}
|
||||||
{%- if not embedded and docstitle %}
|
{%- if not embedded and docstitle %}
|
||||||
{%- set titlesuffix = " — "|safe + docstitle|e %}
|
{%- set titlesuffix = " — "|safe + docstitle|e %}
|
||||||
{%- else %}
|
{%- else %}
|
||||||
{%- set titlesuffix = "" %}
|
{%- set titlesuffix = "" %}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- set lang_attr = 'en' if language == None else (language | replace('_', '-')) %}
|
|
||||||
|
|
||||||
{# Build sphinx_version_info tuple from sphinx_version string in pure Jinja #}
|
{%- macro relbar() %}
|
||||||
{%- set (_ver_major, _ver_minor) = (sphinx_version.split('.') | list)[:2] | map('int') -%}
|
<div class="related">
|
||||||
{%- set sphinx_version_info = (_ver_major, _ver_minor, -1) -%}
|
<h3>{{ _('Navigation') }}</h3>
|
||||||
|
<ul>
|
||||||
|
{%- for rellink in rellinks %}
|
||||||
|
<li class="right" {% if loop.first %}style="margin-right: 10px"{% endif %}>
|
||||||
|
<a href="{{ pathto(rellink[0]) }}" title="{{ rellink[1]|striptags|e }}"
|
||||||
|
{{ accesskey(rellink[2]) }}>{{ rellink[3] }}</a>
|
||||||
|
{%- if not loop.first %}{{ reldelim2 }}{% endif %}</li>
|
||||||
|
{%- endfor %}
|
||||||
|
{%- block rootrellink %}
|
||||||
|
<li><a href="{{ pathto(master_doc) }}">{{ shorttitle|e }}</a>{{ reldelim1 }}</li>
|
||||||
|
{%- endblock %}
|
||||||
|
{%- for parent in parents %}
|
||||||
|
<li><a href="{{ parent.link|e }}" {% if loop.last %}{{ accesskey("U") }}{% endif %}>{{ parent.title }}</a>{{ reldelim1 }}</li>
|
||||||
|
{%- endfor %}
|
||||||
|
{%- block relbaritems %} {% endblock %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
{%- macro sidebar() %}
|
||||||
<html class="writer-html5" lang="{{ lang_attr }}"{% if sphinx_version_info >= (7, 2) %} data-content_root="{{ content_root }}"{% endif %}>
|
{%- if render_sidebar %}
|
||||||
<head>
|
<div class="sphinxsidebar">
|
||||||
<meta charset="utf-8" />
|
<div class="sphinxsidebarwrapper">
|
||||||
{%- if READTHEDOCS and not embedded %}
|
{%- block sidebarlogo %}
|
||||||
<meta name="readthedocs-addons-api-version" content="1">
|
{%- if logo %}
|
||||||
{%- endif %}
|
<p class="logo"><a href="{{ pathto(master_doc) }}">
|
||||||
{{- metatags }}
|
<img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
</a></p>
|
||||||
{%- block htmltitle %}
|
{%- endif %}
|
||||||
<title>{{ title|striptags|e }}{{ titlesuffix }}</title>
|
{%- endblock %}
|
||||||
{%- endblock -%}
|
{%- if sidebars != None %}
|
||||||
|
{#- new style sidebar: explicitly include/exclude templates #}
|
||||||
|
{%- for sidebartemplate in sidebars %}
|
||||||
|
{%- include sidebartemplate %}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- else %}
|
||||||
|
{#- old style sidebars: using blocks -- should be deprecated #}
|
||||||
|
{%- block sidebartoc %}
|
||||||
|
{%- include "localtoc.html" %}
|
||||||
|
{%- endblock %}
|
||||||
|
{%- block sidebarrel %}
|
||||||
|
{%- include "relations.html" %}
|
||||||
|
{%- endblock %}
|
||||||
|
{%- block sidebarsourcelink %}
|
||||||
|
{%- include "sourcelink.html" %}
|
||||||
|
{%- endblock %}
|
||||||
|
{%- if customsidebar %}
|
||||||
|
{%- include customsidebar %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- block sidebarsearch %}
|
||||||
|
{%- include "searchbox.html" %}
|
||||||
|
{%- endblock %}
|
||||||
|
{%- endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{%- endif %}
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
{#- CSS #}
|
{%- macro script() %}
|
||||||
{%- for css_file in css_files %}
|
<script type="text/javascript">
|
||||||
{%- if css_file|attr("filename") %}
|
var DOCUMENTATION_OPTIONS = {
|
||||||
{{ css_tag(css_file) }}
|
URL_ROOT: '{{ url_root }}',
|
||||||
{%- else %}
|
VERSION: '{{ release|e }}',
|
||||||
<link rel="stylesheet" href="{{ pathto(css_file, 1)|escape }}" type="text/css" />
|
COLLAPSE_INDEX: false,
|
||||||
{%- endif %}
|
FILE_SUFFIX: '{{ '' if no_search_suffix else file_suffix }}',
|
||||||
{%- endfor %}
|
HAS_SOURCE: {{ has_source|lower }},
|
||||||
|
SOURCELINK_SUFFIX: '{{ sourcelink_suffix }}'
|
||||||
{#
|
};
|
||||||
"extra_css_files" is an undocumented Read the Docs theme specific option.
|
</script>
|
||||||
There is no need to check for ``|attr("filename")`` here because it's always a string.
|
|
||||||
Note that this option should be removed in favor of regular ``html_css_files``:
|
|
||||||
https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_css_files
|
|
||||||
#}
|
|
||||||
{%- for css_file in extra_css_files %}
|
|
||||||
<link rel="stylesheet" href="{{ pathto(css_file, 1)|escape }}" type="text/css" />
|
|
||||||
{%- endfor -%}
|
|
||||||
|
|
||||||
{#- FAVICON #}
|
|
||||||
{%- if favicon_url %}
|
|
||||||
<link rel="shortcut icon" href="{{ favicon_url }}"/>
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
{#- CANONICAL URL (deprecated) #}
|
|
||||||
{%- if theme_canonical_url and not pageurl %}
|
|
||||||
<link rel="canonical" href="{{ theme_canonical_url }}{{ pagename }}.html"/>
|
|
||||||
{%- endif -%}
|
|
||||||
|
|
||||||
{#- CANONICAL URL #}
|
|
||||||
{%- if pageurl %}
|
|
||||||
<link rel="canonical" href="{{ pageurl|e }}" />
|
|
||||||
{%- endif -%}
|
|
||||||
|
|
||||||
{#- JAVASCRIPTS #}
|
|
||||||
{%- block scripts %}
|
|
||||||
{%- if not embedded %}
|
|
||||||
{%- for scriptfile in script_files %}
|
{%- for scriptfile in script_files %}
|
||||||
{{ js_tag(scriptfile) }}
|
<script type="text/javascript" src="{{ pathto(scriptfile, 1) }}"></script>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
<script src="{{ pathto('_static/js/theme.js', 1) }}"></script>
|
{%- endmacro %}
|
||||||
|
|
||||||
{%- if READTHEDOCS or DEBUG %}
|
{%- macro css() %}
|
||||||
<script src="{{ pathto('_static/js/versions.js', 1) }}"></script>
|
<link rel="stylesheet" href="{{ pathto('_static/' + style, 1) }}" type="text/css" />
|
||||||
{%- endif %}
|
<link rel="stylesheet" href="{{ pathto('_static/pygments.css', 1) }}" type="text/css" />
|
||||||
|
{%- for cssfile in css_files %}
|
||||||
|
<link rel="stylesheet" href="{{ pathto(cssfile, 1) }}" type="text/css" />
|
||||||
|
{%- endfor %}
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
{#- OPENSEARCH #}
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset={{ encoding }}" />
|
||||||
|
{{ metatags }}
|
||||||
|
{%- block htmltitle %}
|
||||||
|
<title>{{ title|striptags|e }}{{ titlesuffix }}</title>
|
||||||
|
{%- endblock %}
|
||||||
|
{{ css() }}
|
||||||
|
{%- if not embedded %}
|
||||||
|
{{ script() }}
|
||||||
{%- if use_opensearch %}
|
{%- if use_opensearch %}
|
||||||
<link rel="search" type="application/opensearchdescription+xml"
|
<link rel="search" type="application/opensearchdescription+xml"
|
||||||
title="{% trans docstitle=docstitle|e %}Search within {{ docstitle }}{% endtrans %}"
|
title="{% trans docstitle=docstitle|e %}Search within {{ docstitle }}{% endtrans %}"
|
||||||
href="{{ pathto('_static/opensearch.xml', 1) }}"/>
|
href="{{ pathto('_static/opensearch.xml', 1) }}"/>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- endif %}
|
{%- if favicon %}
|
||||||
{%- endblock %}
|
<link rel="shortcut icon" href="{{ pathto('_static/' + favicon, 1) }}"/>
|
||||||
|
{%- endif %}
|
||||||
{%- block linktags %}
|
{%- if theme_canonical_url %}
|
||||||
|
<link rel="canonical" href="{{ theme_canonical_url }}{{ pagename }}.html"/>
|
||||||
|
{%- endif %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- block linktags %}
|
||||||
{%- if hasdoc('about') %}
|
{%- if hasdoc('about') %}
|
||||||
<link rel="author" title="{{ _('About these documents') }}" href="{{ pathto('about') }}" />
|
<link rel="author" title="{{ _('About these documents') }}" href="{{ pathto('about') }}" />
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
@@ -93,135 +143,67 @@
|
|||||||
{%- if hasdoc('copyright') %}
|
{%- if hasdoc('copyright') %}
|
||||||
<link rel="copyright" title="{{ _('Copyright') }}" href="{{ pathto('copyright') }}" />
|
<link rel="copyright" title="{{ _('Copyright') }}" href="{{ pathto('copyright') }}" />
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
<link rel="top" title="{{ docstitle|e }}" href="{{ pathto('index') }}" />
|
||||||
|
{%- if parents %}
|
||||||
|
<link rel="up" title="{{ parents[-1].title|striptags|e }}" href="{{ parents[-1].link|e }}" />
|
||||||
|
{%- endif %}
|
||||||
{%- if next %}
|
{%- if next %}
|
||||||
<link rel="next" title="{{ next.title|striptags|e }}" href="{{ next.link|e }}" />
|
<link rel="next" title="{{ next.title|striptags|e }}" href="{{ next.link|e }}" />
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- if prev %}
|
{%- if prev %}
|
||||||
<link rel="prev" title="{{ prev.title|striptags|e }}" href="{{ prev.link|e }}" />
|
<link rel="prev" title="{{ prev.title|striptags|e }}" href="{{ prev.link|e }}" />
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- endblock %}
|
{%- endblock %}
|
||||||
{%- block extrahead %} {% endblock %}
|
{%- block extrahead %} {% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
<body>
|
||||||
|
{%- block header %}{% endblock %}
|
||||||
|
|
||||||
<body class="wy-body-for-nav">
|
{%- block relbar1 %}{{ relbar() }}{% endblock %}
|
||||||
|
|
||||||
{%- block extrabody %} {% endblock %}
|
{%- block content %}
|
||||||
<div class="wy-grid-for-nav">
|
{%- block sidebar1 %} {# possible location for sidebar #} {% endblock %}
|
||||||
{#- SIDE NAV, TOGGLES ON MOBILE #}
|
|
||||||
<nav data-toggle="wy-nav-shift" class="wy-nav-side">
|
|
||||||
<div class="wy-side-scroll">
|
|
||||||
<div class="wy-side-nav-search" {% if theme_style_nav_header_background %} style="background: {{theme_style_nav_header_background}}" {% endif %}>
|
|
||||||
{%- block sidebartitle %}
|
|
||||||
|
|
||||||
{# the logo helper function was removed in Sphinx 6 and deprecated since Sphinx 4 #}
|
<div class="document">
|
||||||
{# the master_doc variable was renamed to root_doc in Sphinx 4 (master_doc still exists in later Sphinx versions) #}
|
{%- block document %}
|
||||||
{%- set _logo_url = logo_url|default(pathto('_static/' + (logo or ""), 1)) %}
|
<div class="documentwrapper">
|
||||||
{%- set _root_doc = root_doc|default(master_doc) %}
|
{%- if render_sidebar %}
|
||||||
<a href="{{ pathto(_root_doc) }}"{% if not theme_logo_only %} class="icon icon-home"{% endif %}>
|
<div class="bodywrapper">
|
||||||
{% if not theme_logo_only %}{{ project }}{% endif %}
|
{%- endif %}
|
||||||
{%- if logo or logo_url %}
|
<div class="body">
|
||||||
<img src="{{ _logo_url }}" class="logo" alt="{{ _('Logo') }}"/>
|
{% block body %} {% endblock %}
|
||||||
{%- endif %}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{%- if READTHEDOCS or DEBUG %}
|
|
||||||
{%- if theme_version_selector or theme_language_selector %}
|
|
||||||
<div class="switch-menus">
|
|
||||||
<div class="version-switch"></div>
|
|
||||||
<div class="language-switch"></div>
|
|
||||||
</div>
|
|
||||||
{%- endif %}
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
{%- include "searchbox.html" %}
|
|
||||||
|
|
||||||
{%- endblock %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{%- block navigation %}
|
|
||||||
{#- Translators: This is an ARIA section label for the main navigation menu -#}
|
|
||||||
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="{{ _('Navigation menu') }}">
|
|
||||||
{%- block menu %}
|
|
||||||
{%- set toctree = toctree(maxdepth=theme_navigation_depth|int,
|
|
||||||
collapse=theme_collapse_navigation|tobool,
|
|
||||||
includehidden=theme_includehidden|tobool,
|
|
||||||
titles_only=theme_titles_only|tobool) %}
|
|
||||||
{%- if toctree %}
|
|
||||||
{{ toctree }}
|
|
||||||
{%- else %}
|
|
||||||
<!-- Local TOC -->
|
|
||||||
<div class="local-toc">{{ toc }}</div>
|
|
||||||
{%- endif %}
|
|
||||||
{%- endblock %}
|
|
||||||
</div>
|
|
||||||
{%- endblock %}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
|
|
||||||
|
|
||||||
{#- MOBILE NAV, TRIGGLES SIDE NAV ON TOGGLE #}
|
|
||||||
{#- Translators: This is an ARIA section label for the navigation menu that is visible when viewing the page on mobile devices -#}
|
|
||||||
<nav class="wy-nav-top" aria-label="{{ _('Mobile navigation menu') }}" {% if theme_style_nav_header_background %} style="background: {{theme_style_nav_header_background}}" {% endif %}>
|
|
||||||
{%- block mobile_nav %}
|
|
||||||
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
|
|
||||||
<a href="{{ pathto(master_doc) }}">{{ project }}</a>
|
|
||||||
{%- endblock %}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="wy-nav-content">
|
|
||||||
{%- block content %}
|
|
||||||
{%- if theme_style_external_links|tobool %}
|
|
||||||
<div class="rst-content style-external-links">
|
|
||||||
{%- else %}
|
|
||||||
<div class="rst-content">
|
|
||||||
{%- endif %}
|
|
||||||
{% include "breadcrumbs.html" %}
|
|
||||||
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
|
|
||||||
{%- block document %}
|
|
||||||
<div itemprop="articleBody">
|
|
||||||
{% block body %}{% endblock %}
|
|
||||||
</div>
|
|
||||||
{%- if self.comments()|trim %}
|
|
||||||
<div class="articleComments">
|
|
||||||
{%- block comments %}{% endblock %}
|
|
||||||
</div>
|
|
||||||
{%- endif%}
|
|
||||||
</div>
|
</div>
|
||||||
{%- endblock %}
|
{%- if render_sidebar %}
|
||||||
{% include "footer.html" %}
|
|
||||||
</div>
|
</div>
|
||||||
{%- endblock %}
|
{%- endif %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
{%- endblock %}
|
||||||
</div>
|
|
||||||
{% include "versions.html" -%}
|
|
||||||
|
|
||||||
<script>
|
{%- block sidebar2 %}{{ sidebar() }}{% endblock %}
|
||||||
jQuery(function () {
|
<div class="clearer"></div>
|
||||||
SphinxRtdTheme.Navigation.enable({{ 'true' if theme_sticky_navigation|tobool else 'false' }});
|
</div>
|
||||||
});
|
{%- endblock %}
|
||||||
</script>
|
|
||||||
|
|
||||||
{#- Do not conflict with RTD insertion of analytics script #}
|
{%- block relbar2 %}{{ relbar() }}{% endblock %}
|
||||||
{%- if not READTHEDOCS %}
|
|
||||||
{%- if theme_analytics_id %}
|
|
||||||
<!-- Theme Analytics -->
|
|
||||||
<script async src="https://www.googletagmanager.com/gtag/js?id={{ theme_analytics_id }}"></script>
|
|
||||||
<script>
|
|
||||||
window.dataLayer = window.dataLayer || [];
|
|
||||||
function gtag(){dataLayer.push(arguments);}
|
|
||||||
gtag('js', new Date());
|
|
||||||
|
|
||||||
gtag('config', '{{ theme_analytics_id }}', {
|
|
||||||
'anonymize_ip': {{ 'true' if theme_analytics_anonymize_ip|tobool else 'false' }},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
{%- block footer %}
|
||||||
|
<div class="footer">
|
||||||
|
{%- if show_copyright %}
|
||||||
|
{%- if hasdoc('copyright') %}
|
||||||
|
{% trans path=pathto('copyright'), copyright=copyright|e %}© <a href="{{ path }}">Copyright</a> {{ copyright }}.{% endtrans %}
|
||||||
|
{%- else %}
|
||||||
|
{% trans copyright=copyright|e %}© Copyright {{ copyright }}.{% endtrans %}
|
||||||
|
{%- endif %}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- endif %}
|
{%- if last_updated %}
|
||||||
|
{% trans last_updated=last_updated|e %}Last updated on {{ last_updated }}.{% endtrans %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- if show_sphinx %}
|
||||||
|
{% trans sphinx_version=sphinx_version|e %}Created using <a href="http://sphinx-doc.org/">Sphinx</a> {{ sphinx_version }}.{% endtrans %}
|
||||||
|
{%- endif %}
|
||||||
|
</div>
|
||||||
|
<p>asdf asdf asdf asdf 22</p>
|
||||||
|
{%- endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
{%- block footer %} {% endblock %}
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -39,7 +39,7 @@ as well as the type of underlying hardware. Example:
|
|||||||
"rsa_pubkey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqh…nswIDAQAB\n-----END PUBLIC KEY-----\n"
|
"rsa_pubkey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqh…nswIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
The ``rsa_pubkey`` is optional any only required for certain features such as working with reusable
|
The ``rsa_pubkey`` is optional any only required for certain fatures such as working with reusable
|
||||||
media and NFC cryptography.
|
media and NFC cryptography.
|
||||||
|
|
||||||
Every initialization token can only be used once. On success, you will receive a response containing
|
Every initialization token can only be used once. On success, you will receive a response containing
|
||||||
@@ -197,11 +197,10 @@ Permissions & security profiles
|
|||||||
|
|
||||||
Device authentication is currently hardcoded to grant the following permissions:
|
Device authentication is currently hardcoded to grant the following permissions:
|
||||||
|
|
||||||
* Read event meta data and products etc.
|
* View event meta data and products etc.
|
||||||
* Read and write orders
|
* View orders
|
||||||
* Read and write gift cards
|
* Change orders
|
||||||
* Read and write reusable media
|
* Manage gift cards
|
||||||
* Read vouchers
|
|
||||||
|
|
||||||
Devices cannot change events or products and cannot access vouchers.
|
Devices cannot change events or products and cannot access vouchers.
|
||||||
|
|
||||||
@@ -209,6 +208,20 @@ Additionally, when creating a device through the user interface or API, a user c
|
|||||||
the device. These include an allow list of specific API calls that may be made by the device. pretix ships with security
|
the device. These include an allow list of specific API calls that may be made by the device. pretix ships with security
|
||||||
policies for official pretix apps like pretixSCAN and pretixPOS.
|
policies for official pretix apps like pretixSCAN and pretixPOS.
|
||||||
|
|
||||||
|
Removing a device
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
If you want implement a way to to deprovision a device in your software, you can call the ``revoke`` endpoint to
|
||||||
|
invalidate your API key. There is no way to reverse this operation.
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/device/revoke HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Authorization: Device 1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd
|
||||||
|
|
||||||
|
This can also be done by the user through the web interface.
|
||||||
|
|
||||||
Event selection
|
Event selection
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ List-level conditional fetching
|
|||||||
If modification checks are not possible with this granularity, you can instead check for the full list.
|
If modification checks are not possible with this granularity, you can instead check for the full list.
|
||||||
In this case, the list of objects may contain a regular HTTP header ``Last-Modified`` with the date of the
|
In this case, the list of objects may contain a regular HTTP header ``Last-Modified`` with the date of the
|
||||||
last modification to any item of that resource. You can then pass this date back in your next request in the
|
last modification to any item of that resource. You can then pass this date back in your next request in the
|
||||||
``If-Modified-Since`` header. If any object has changed in the meantime, you will receive back a full list
|
``If-Modified-Since`` header. If the any object has changed in the meantime, you will receive back a full list
|
||||||
(if something it missing, this means the object has been deleted). If nothing happened, we'll send back a
|
(if something it missing, this means the object has been deleted). If nothing happened, we'll send back a
|
||||||
``304 Not Modified`` return code.
|
``304 Not Modified`` return code.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ software_brand string Device software
|
|||||||
software_version string Device software version (read-only)
|
software_version string Device software version (read-only)
|
||||||
created datetime Creation time
|
created datetime Creation time
|
||||||
initialized datetime Time of initialization (or ``null``)
|
initialized datetime Time of initialization (or ``null``)
|
||||||
initialization_token string Token for initialization (field invisible without write permission)
|
initialization_token string Token for initialization
|
||||||
revoked boolean Whether this device no longer has access
|
revoked boolean Whether this device no longer has access
|
||||||
security_profile string The name of a supported security profile restricting API access
|
security_profile string The name of a supported security profile restricting API access
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ Endpoints
|
|||||||
|
|
||||||
Returns a list of all events within a given organizer the authenticated user/token has access to.
|
Returns a list of all events within a given organizer the authenticated user/token has access to.
|
||||||
|
|
||||||
|
Permission required: "Can change event settings"
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -159,6 +161,8 @@ Endpoints
|
|||||||
|
|
||||||
Returns information on one event, identified by its slug.
|
Returns information on one event, identified by its slug.
|
||||||
|
|
||||||
|
Permission required: "Can change event settings"
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -230,6 +234,8 @@ Endpoints
|
|||||||
Please note that events cannot be created as 'live' using this endpoint. Quotas and payment must be added to the
|
Please note that events cannot be created as 'live' using this endpoint. Quotas and payment must be added to the
|
||||||
event before sales can go live.
|
event before sales can go live.
|
||||||
|
|
||||||
|
Permission required: "Can create events"
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -332,6 +338,8 @@ Endpoints
|
|||||||
Please note that you can only copy from events under the same organizer this way. Use the ``clone_from`` parameter
|
Please note that you can only copy from events under the same organizer this way. Use the ``clone_from`` parameter
|
||||||
when creating a new event for this instead.
|
when creating a new event for this instead.
|
||||||
|
|
||||||
|
Permission required: "Can create events"
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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.
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ Endpoints
|
|||||||
.. http:post:: /api/v1/organizers/(organizer)/reusablemedia/lookup/
|
.. http:post:: /api/v1/organizers/(organizer)/reusablemedia/lookup/
|
||||||
|
|
||||||
Look up a new reusable medium by its identifier. In some cases, this might lead to the automatic creation of a new
|
Look up a new reusable medium by its identifier. In some cases, this might lead to the automatic creation of a new
|
||||||
medium behind the scenes, therefore this endpoint requires write permissions.
|
medium behind the scenes.
|
||||||
|
|
||||||
This endpoint, and this endpoint only, might return media from a different organizer if there is a cross-acceptance
|
This endpoint, and this endpoint only, might return media from a different organizer if there is a cross-acceptance
|
||||||
agreement. In this case, only linked gift cards will be returned, no order position or customer records,
|
agreement. In this case, only linked gift cards will be returned, no order position or customer records,
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ Endpoints
|
|||||||
|
|
||||||
Creates a new subevent.
|
Creates a new subevent.
|
||||||
|
|
||||||
|
Permission required: "Can create events"
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -298,6 +300,8 @@ Endpoints
|
|||||||
provide all fields of the resource, other fields will be reset to default. With ``PATCH``, you only need to provide
|
provide all fields of the resource, other fields will be reset to default. With ``PATCH``, you only need to provide
|
||||||
the fields that you want to change.
|
the fields that you want to change.
|
||||||
|
|
||||||
|
Permission required: "Can change event settings"
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@@ -369,6 +373,8 @@ Endpoints
|
|||||||
|
|
||||||
Delete a sub-event. Note that events with orders cannot be deleted to ensure data integrity.
|
Delete a sub-event. Note that events with orders cannot be deleted to ensure data integrity.
|
||||||
|
|
||||||
|
Permission required: "Can change event settings"
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
|
|||||||
@@ -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,
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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``
|
||||||
|
|||||||
@@ -178,124 +178,3 @@ Flowchart
|
|||||||
---------
|
---------
|
||||||
|
|
||||||
.. image:: /images/cart_pricing.png
|
.. image:: /images/cart_pricing.png
|
||||||
|
|
||||||
|
|
||||||
.. _`algorithms-rounding`:
|
|
||||||
|
|
||||||
Rounding of taxes
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
pretix internally always stores taxes on a per-line level, like this:
|
|
||||||
|
|
||||||
========== ========== =========== ======= =============
|
|
||||||
Product Tax rate Net price Tax Gross price
|
|
||||||
========== ========== =========== ======= =============
|
|
||||||
Ticket A 19 % 84.03 15.97 100.00
|
|
||||||
Ticket B 19 % 84.03 15.97 100.00
|
|
||||||
Ticket C 19 % 84.03 15.97 100.00
|
|
||||||
Ticket D 19 % 84.03 15.97 100.00
|
|
||||||
Ticket E 19 % 84.03 15.97 100.00
|
|
||||||
Sum 420.15 79.85 500.00
|
|
||||||
========== ========== =========== ======= =============
|
|
||||||
|
|
||||||
Whether the net price is computed from the gross price or vice versa is configured on the tax rule and may differ for every line.
|
|
||||||
|
|
||||||
The line-based computation has a few significant advantages:
|
|
||||||
|
|
||||||
- We can report both net and gross prices for every individual ticket.
|
|
||||||
|
|
||||||
- We can report both net and gross prices for every filter imaginable, such as the gross sum of all sales of Ticket A
|
|
||||||
or the net sum of all sales for a specific date in an event series. All numbers will be exact.
|
|
||||||
|
|
||||||
- When splitting the order into two, both net price and gross price are split without any changes in rounding.
|
|
||||||
|
|
||||||
The main disadvantage is that the tax looks "wrong" when computed from the sum. Taking the sum of net prices (420.15)
|
|
||||||
and multiplying it with the tax rate (19%) yields a tax amount of 79.83 (instead of 79.85) and a gross sum of 499.98
|
|
||||||
(instead of 500.00). This becomes a problem when juristictions, data formats, or external systems expect this calculation
|
|
||||||
to work on the level of the entire order. A prominent example is the EN 16931 standard for e-invoicing that
|
|
||||||
does not allow the computation as created by pretix.
|
|
||||||
|
|
||||||
However, calculating the tax rate from the net total has significant disadvantages:
|
|
||||||
|
|
||||||
- It is impossible to guarantee a stable gross price this way, i.e. if you advertise a price of €100 per ticket to
|
|
||||||
consumers, they will be confused when they only need to pay €499.98 for 5 tickets.
|
|
||||||
|
|
||||||
- Some prices are impossible, e.g. you cannot sell a ticket for a gross price of €99.99 at a 19% tax rate, since there
|
|
||||||
is no two-decimal net price that would be computed to a gross price of €99.99.
|
|
||||||
|
|
||||||
- When splitting an order into two, the combined of the new orders is not guaranteed to be the same as the total of the
|
|
||||||
original order. Therefore, additional payments or refunds of very small amounts might be necessary.
|
|
||||||
|
|
||||||
To allow organizers to make their own choices on this matter, pretix provides the following options:
|
|
||||||
|
|
||||||
Compute taxes for every line individually
|
|
||||||
"""""""""""""""""""""""""""""""""""""""""
|
|
||||||
|
|
||||||
Algorithm identifier: ``line``
|
|
||||||
|
|
||||||
This is our original algorithm where the tax value is rounded for every line individually.
|
|
||||||
|
|
||||||
**This is our current default algorithm and we recommend it whenever you do not have different requirements** (see below).
|
|
||||||
For the example above:
|
|
||||||
|
|
||||||
========== ========== =========== ======= =============
|
|
||||||
Product Tax rate Net price Tax Gross price
|
|
||||||
========== ========== =========== ======= =============
|
|
||||||
Ticket A 19 % 84.03 15.97 100.00
|
|
||||||
Ticket B 19 % 84.03 15.97 100.00
|
|
||||||
Ticket C 19 % 84.03 15.97 100.00
|
|
||||||
Ticket D 19 % 84.03 15.97 100.00
|
|
||||||
Ticket E 19 % 84.03 15.97 100.00
|
|
||||||
Sum 420.15 79.85 500.00
|
|
||||||
========== ========== =========== ======= =============
|
|
||||||
|
|
||||||
|
|
||||||
Compute taxes based on net total
|
|
||||||
""""""""""""""""""""""""""""""""
|
|
||||||
|
|
||||||
Algorithm identifier: ``sum_by_net``
|
|
||||||
|
|
||||||
In this algorithm, the tax value and gross total are computed from the sum of the net prices. To accomplish this within
|
|
||||||
our data model, the gross price and tax of some of the tickets will be changed by the minimum currency unit (e.g. €0.01).
|
|
||||||
The net price of the tickets always stay the same.
|
|
||||||
|
|
||||||
**This is the algorithm intended by EN 16931 invoices and our recommendation to use for e-invoicing when (primarily) business customers are involved.**
|
|
||||||
|
|
||||||
The main downside is that it might be confusing when selling to consumers, since the amounts to be paid change in unexpected ways.
|
|
||||||
For the example above, the customer expects to pay 5 times 100.00, but they are are in fact charged 499.98:
|
|
||||||
|
|
||||||
========== ========== =========== ============================== ==============================
|
|
||||||
Product Tax rate Net price Tax Gross price
|
|
||||||
========== ========== =========== ============================== ==============================
|
|
||||||
Ticket A 19 % 84.03 15.96 (incl. -0.01 rounding) 99.99 (incl. -0.01 rounding)
|
|
||||||
Ticket B 19 % 84.03 15.96 (incl. -0.01 rounding) 99.99 (incl. -0.01 rounding)
|
|
||||||
Ticket C 19 % 84.03 15.97 100.00
|
|
||||||
Ticket D 19 % 84.03 15.97 100.00
|
|
||||||
Ticket E 19 % 84.03 15.97 100.00
|
|
||||||
Sum 420.15 78.83 499.98
|
|
||||||
========== ========== =========== ============================== ==============================
|
|
||||||
|
|
||||||
Compute taxes based on net total with stable gross prices
|
|
||||||
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
|
||||||
|
|
||||||
Algorithm identifier: ``sum_by_net_keep_gross``
|
|
||||||
|
|
||||||
In this algorithm, the tax value and gross total are computed from the sum of the net prices. However, the net prices
|
|
||||||
of some of the tickets will be changed automatically by the minimum currency unit (e.g. €0.01) such that the resulting
|
|
||||||
gross prices stay the same.
|
|
||||||
|
|
||||||
**This is less confusing to consumers and the end result is still compliant to EN 16931, so we recommend this for e-invoicing when (primarily) consumers are involved.**
|
|
||||||
|
|
||||||
The main downside is that it might be confusing when selling to business customers, since the prices of the identical tickets appear to be different.
|
|
||||||
Full computation for the example above:
|
|
||||||
|
|
||||||
========== ========== ============================= ============================== =============
|
|
||||||
Product Tax rate Net price Tax Gross price
|
|
||||||
========== ========== ============================= ============================== =============
|
|
||||||
Ticket A 19 % 84.04 (incl. 0.01 rounding) 15.96 (incl. -0.01 rounding) 100.00
|
|
||||||
Ticket B 19 % 84.04 (incl. 0.01 rounding) 15.96 (incl. -0.01 rounding) 100.00
|
|
||||||
Ticket C 19 % 84.03 15.97 100.00
|
|
||||||
Ticket D 19 % 84.03 15.97 100.00
|
|
||||||
Ticket E 19 % 84.03 15.97 100.00
|
|
||||||
Sum 420.17 79.83 500.00
|
|
||||||
========== ========== ============================= ============================== =============
|
|
||||||
|
|||||||
@@ -55,12 +55,12 @@ your views:
|
|||||||
)
|
)
|
||||||
|
|
||||||
class AdminView(EventPermissionRequiredMixin, View):
|
class AdminView(EventPermissionRequiredMixin, View):
|
||||||
permission = 'event.orders:read'
|
permission = 'can_view_orders'
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
@event_permission_required('event.orders:read')
|
@event_permission_required('can_view_orders')
|
||||||
def admin_view(request, organizer, event):
|
def admin_view(request, organizer, event):
|
||||||
...
|
...
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ event-related views, there is also a signal that allows you to add the view to t
|
|||||||
@receiver(nav_event, dispatch_uid='friends_tickets_nav')
|
@receiver(nav_event, dispatch_uid='friends_tickets_nav')
|
||||||
def navbar_info(sender, request, **kwargs):
|
def navbar_info(sender, request, **kwargs):
|
||||||
url = resolve(request.path_info)
|
url = resolve(request.path_info)
|
||||||
if not request.user.has_event_permission(request.organizer, request.event, 'event.vouchers:read'):
|
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_vouchers'):
|
||||||
return []
|
return []
|
||||||
return [{
|
return [{
|
||||||
'label': _('My plugin view'),
|
'label': _('My plugin view'),
|
||||||
@@ -118,7 +118,7 @@ for good integration. If you just want to display a form, you could do it like t
|
|||||||
|
|
||||||
class MySettingsView(EventSettingsViewMixin, EventSettingsFormView):
|
class MySettingsView(EventSettingsViewMixin, EventSettingsFormView):
|
||||||
model = Event
|
model = Event
|
||||||
permission = 'event.settings.general:write'
|
permission = 'can_change_settings'
|
||||||
form_class = MySettingsForm
|
form_class = MySettingsForm
|
||||||
template_name = 'my_plugin/settings.html'
|
template_name = 'my_plugin/settings.html'
|
||||||
|
|
||||||
@@ -204,13 +204,13 @@ In case of ``orga_router`` and ``event_router``, permission checking is done for
|
|||||||
in the control panel. However, you need to make sure on your own only to return the correct subset of data! ``request
|
in the control panel. However, you need to make sure on your own only to return the correct subset of data! ``request
|
||||||
.event`` and ``request.organizer`` are available as usual.
|
.event`` and ``request.organizer`` are available as usual.
|
||||||
|
|
||||||
To require a special permission like ``event.orders:read``, you do not need to inherit from a special ViewSet base
|
To require a special permission like ``can_view_orders``, you do not need to inherit from a special ViewSet base
|
||||||
class, you can just set the ``permission`` attribute on your viewset:
|
class, you can just set the ``permission`` attribute on your viewset:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
class MyViewSet(ModelViewSet):
|
class MyViewSet(ModelViewSet):
|
||||||
permission = 'event.orders:read'
|
permission = 'can_view_orders'
|
||||||
...
|
...
|
||||||
|
|
||||||
If you want to check the permission only for some methods of your viewset, you have to do it yourself. Note here that
|
If you want to check the permission only for some methods of your viewset, you have to do it yourself. Note here that
|
||||||
@@ -220,7 +220,7 @@ following:
|
|||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken) else request.user)
|
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken) else request.user)
|
||||||
if perm_holder.has_event_permission(request.event.organizer, request.event, 'event.orders:read'):
|
if perm_holder.has_event_permission(request.event.organizer, request.event, 'can_view_orders'):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -80,24 +80,8 @@ The exporter class
|
|||||||
|
|
||||||
.. autoattribute:: category
|
.. autoattribute:: category
|
||||||
|
|
||||||
.. autoattribute:: feature
|
|
||||||
|
|
||||||
.. autoattribute:: export_form_fields
|
.. autoattribute:: export_form_fields
|
||||||
|
|
||||||
.. autoattribute:: repeatable_read
|
|
||||||
|
|
||||||
.. automethod:: render
|
.. automethod:: render
|
||||||
|
|
||||||
This is an abstract method, you **must** override this!
|
This is an abstract method, you **must** override this!
|
||||||
|
|
||||||
.. automethod:: available_for_user
|
|
||||||
|
|
||||||
.. automethod:: get_required_event_permission
|
|
||||||
|
|
||||||
On organizer level, by default exporters are expected to handle on a *set of events* and the system will automatically
|
|
||||||
add a form field that allows the selection of events, limited to events the user has correct permissions for. If this
|
|
||||||
does not fit your organizer, because it is not related to events, you should **also** inherit from the following class:
|
|
||||||
|
|
||||||
.. class:: pretix.base.exporter.OrganizerLevelExportMixin
|
|
||||||
|
|
||||||
.. automethod:: get_required_organizer_permission
|
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ Core
|
|||||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
|
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
|
||||||
item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter,
|
item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter,
|
||||||
register_ticket_secret_generators, gift_card_transaction_display,
|
register_ticket_secret_generators, gift_card_transaction_display,
|
||||||
register_text_placeholders, register_mail_placeholders, device_info_updated,
|
register_text_placeholders, register_mail_placeholders, device_info_updated
|
||||||
register_event_permission_groups, register_organizer_permission_groups
|
|
||||||
|
|
||||||
Order events
|
Order events
|
||||||
""""""""""""
|
""""""""""""
|
||||||
@@ -24,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
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ A simple implementation could look like this:
|
|||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
class MyNotificationType(NotificationType):
|
class MyNotificationType(NotificationType):
|
||||||
required_permission = "event.orders:read"
|
required_permission = "can_view_orders"
|
||||||
action_type = "pretix.event.order.paid"
|
action_type = "pretix.event.order.paid"
|
||||||
verbose_name = _("Order has been paid")
|
verbose_name = _("Order has been paid")
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ Permissions
|
|||||||
===========
|
===========
|
||||||
|
|
||||||
pretix uses a fine-grained permission system to control who is allowed to control what parts of the system.
|
pretix uses a fine-grained permission system to control who is allowed to control what parts of the system.
|
||||||
The central concept here is the concept of *Teams*. You can read more on `configuring teams and permissions`_
|
The central concept here is the concept of *Teams*. You can read more on `configuring teams and permissions <user-teams>`_
|
||||||
and the :class:`pretix.base.models.Team` model in the respective parts of the documentation. The basic digest is:
|
and the :class:`pretix.base.models.Team` model in the respective parts of the documentation. The basic digest is:
|
||||||
An organizer account can have any number of teams, and any number of users can be part of a team. A team can be
|
An organizer account can have any number of teams, and any number of users can be part of a team. A team can be
|
||||||
assigned a set of permissions and connected to some or all of the events of the organizer.
|
assigned a set of permissions and connected to some or all of the events of the organizer.
|
||||||
@@ -25,8 +25,8 @@ permission level to access a view:
|
|||||||
|
|
||||||
|
|
||||||
class MyOrgaView(OrganizerPermissionRequiredMixin, View):
|
class MyOrgaView(OrganizerPermissionRequiredMixin, View):
|
||||||
permission = 'organizer.settings.general:write'
|
permission = 'can_change_organizer_settings'
|
||||||
# Only users with the permission ``organizer.settings.general:write`` on
|
# Only users with the permission ``can_change_organizer_settings`` on
|
||||||
# this organizer can access this
|
# this organizer can access this
|
||||||
|
|
||||||
|
|
||||||
@@ -35,9 +35,9 @@ permission level to access a view:
|
|||||||
# Only users with *any* permission on this organizer can access this
|
# Only users with *any* permission on this organizer can access this
|
||||||
|
|
||||||
|
|
||||||
@organizer_permission_required('organizer.settings.general:write')
|
@organizer_permission_required('can_change_organizer_settings')
|
||||||
def my_orga_view(request, organizer, **kwargs):
|
def my_orga_view(request, organizer, **kwargs):
|
||||||
# Only users with the permission ``organizer.settings.general:write`` on
|
# Only users with the permission ``can_change_organizer_settings`` on
|
||||||
# this organizer can access this
|
# this organizer can access this
|
||||||
|
|
||||||
|
|
||||||
@@ -56,8 +56,8 @@ Of course, the same is available on event level:
|
|||||||
|
|
||||||
|
|
||||||
class MyEventView(EventPermissionRequiredMixin, View):
|
class MyEventView(EventPermissionRequiredMixin, View):
|
||||||
permission = 'event.settings.general:write'
|
permission = 'can_change_event_settings'
|
||||||
# Only users with the permission ``event.settings.general:write`` on
|
# Only users with the permission ``can_change_event_settings`` on
|
||||||
# this event can access this
|
# this event can access this
|
||||||
|
|
||||||
|
|
||||||
@@ -65,16 +65,13 @@ Of course, the same is available on event level:
|
|||||||
permission = None
|
permission = None
|
||||||
# Only users with *any* permission on this event can access this
|
# Only users with *any* permission on this event can access this
|
||||||
|
|
||||||
class MyThirdEventView(EventPermissionRequiredMixin, View):
|
|
||||||
permission = AnyPermissionOf('event.settings.payment:write', 'event.settings.general:write')
|
|
||||||
# Only users with at least one of the specified permissions on this event
|
|
||||||
# can access this
|
|
||||||
|
|
||||||
@event_permission_required('event.settings.general:write')
|
@event_permission_required('can_change_event_settings')
|
||||||
def my_event_view(request, organizer, **kwargs):
|
def my_event_view(request, organizer, **kwargs):
|
||||||
# Only users with the permission ``event.settings.general:write`` on
|
# Only users with the permission ``can_change_event_settings`` on
|
||||||
# this event can access this
|
# this event can access this
|
||||||
|
|
||||||
|
|
||||||
@event_permission_required()
|
@event_permission_required()
|
||||||
def my_other_event_view(request, organizer, **kwargs):
|
def my_other_event_view(request, organizer, **kwargs):
|
||||||
# Only users with *any* permission on this event can access this
|
# Only users with *any* permission on this event can access this
|
||||||
@@ -124,7 +121,7 @@ When creating your own ``viewset`` using Django REST framework, you just need to
|
|||||||
and pretix will check it automatically for you::
|
and pretix will check it automatically for you::
|
||||||
|
|
||||||
class MyModelViewSet(viewsets.ReadOnlyModelViewSet):
|
class MyModelViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
permission = 'event.orders:read'
|
permission = 'can_view_orders'
|
||||||
|
|
||||||
Checking permission in code
|
Checking permission in code
|
||||||
---------------------------
|
---------------------------
|
||||||
@@ -139,12 +136,12 @@ Return all users that are in any team that is connected to this event::
|
|||||||
|
|
||||||
Return all users that are in a team with a specific permission for this event::
|
Return all users that are in a team with a specific permission for this event::
|
||||||
|
|
||||||
>>> event.get_users_with_permission('event.orders:read')
|
>>> event.get_users_with_permission('can_change_event_settings')
|
||||||
<QuerySet: …>
|
<QuerySet: …>
|
||||||
|
|
||||||
Determine if a user has a certain permission for a specific event::
|
Determine if a user has a certain permission for a specific event::
|
||||||
|
|
||||||
>>> user.has_event_permission(organizer, event, 'event.orders:read', request=request)
|
>>> user.has_event_permission(organizer, event, 'can_change_event_settings', request=request)
|
||||||
True
|
True
|
||||||
|
|
||||||
Determine if a user has any permission for a specific event::
|
Determine if a user has any permission for a specific event::
|
||||||
@@ -156,27 +153,27 @@ In the two previous commands, the ``request`` argument is optional, but required
|
|||||||
|
|
||||||
The same method exists for organizer-level permissions::
|
The same method exists for organizer-level permissions::
|
||||||
|
|
||||||
>>> user.has_organizer_permission(organizer, 'event.orders:read', request=request)
|
>>> user.has_organizer_permission(organizer, 'can_change_event_settings', request=request)
|
||||||
True
|
True
|
||||||
|
|
||||||
Sometimes, it might be more useful to get the set of permissions at once::
|
Sometimes, it might be more useful to get the set of permissions at once::
|
||||||
|
|
||||||
>>> user.get_event_permission_set(organizer, event)
|
>>> user.get_event_permission_set(organizer, event)
|
||||||
{'event.settings.general:write', 'event.orders:read', 'event.orders:write'}
|
{'can_change_event_settings', 'can_view_orders', 'can_change_orders'}
|
||||||
|
|
||||||
>>> user.get_organizer_permission_set(organizer, event)
|
>>> user.get_organizer_permission_set(organizer, event)
|
||||||
{'organizer.settings.general:write', 'organizer.events:create'}
|
{'can_change_organizer_settings', 'can_create_events'}
|
||||||
|
|
||||||
Within a view on the ``/control`` subpath, the results of these two methods are already available in the
|
Within a view on the ``/control`` subpath, the results of these two methods are already available in the
|
||||||
``request.eventpermset`` and ``request.orgapermset`` properties. This makes it convenient to query them in templates::
|
``request.eventpermset`` and ``request.orgapermset`` properties. This makes it convenient to query them in templates::
|
||||||
|
|
||||||
{% if "event.orders:write" in request.eventpermset %}
|
{% if "can_change_orders" in request.eventpermset %}
|
||||||
…
|
…
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
You can also do the reverse to get any events a user has access to::
|
You can also do the reverse to get any events a user has access to::
|
||||||
|
|
||||||
>>> user.get_events_with_permission('event.settings.general:write', request=request)
|
>>> user.get_events_with_permission('can_change_event_settings', request=request)
|
||||||
<QuerySet: …>
|
<QuerySet: …>
|
||||||
|
|
||||||
>>> user.get_events_with_any_permission(request=request)
|
>>> user.get_events_with_any_permission(request=request)
|
||||||
@@ -198,53 +195,3 @@ staff mode is active. You can check if a user is in staff mode using their sessi
|
|||||||
Staff mode has a hard time limit and during staff mode, a middleware will log all requests made by that user. Later,
|
Staff mode has a hard time limit and during staff mode, a middleware will log all requests made by that user. Later,
|
||||||
the user is able to also save a message to comment on what they did in their administrative session. This feature is
|
the user is able to also save a message to comment on what they did in their administrative session. This feature is
|
||||||
intended to help compliance with data protection rules as imposed e.g. by GDPR.
|
intended to help compliance with data protection rules as imposed e.g. by GDPR.
|
||||||
|
|
||||||
Adding permissions
|
|
||||||
------------------
|
|
||||||
|
|
||||||
Plugins can add permissions through the ``register_event_permission_groups`` and ``register_organizer_permission_groups``.
|
|
||||||
We recommend to use this only for very significant permissions, as the system will become less usable with too many
|
|
||||||
permission levels, also because the team page will show all permission options, even those of disabled plugins.
|
|
||||||
|
|
||||||
To register your permissions, you need to register a **permission group** (often representing an area of functionality
|
|
||||||
or a key model). Below that group, there are **actions**, which represent the actual permissions. Permissions will be
|
|
||||||
generated as ``<group_name>:<action>``. Then, you need to define **options** which are the valid combinations of the
|
|
||||||
actions that should be possible to select for a team. This two-step mechanism exists to provide a better user experience
|
|
||||||
and avoid useless combinations like "write but not read".
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
@receiver(register_event_permission_groups)
|
|
||||||
def register_plugin_event_permissions(sender, **kwargs):
|
|
||||||
return [
|
|
||||||
PermissionGroup(
|
|
||||||
name="pretix_myplugin.resource",
|
|
||||||
label=_("Resources"),
|
|
||||||
actions=["read", "write"],
|
|
||||||
options=[
|
|
||||||
PermissionOption(actions=tuple(), label=_("No access")),
|
|
||||||
PermissionOption(actions=("read",), label=_("View")),
|
|
||||||
PermissionOption(actions=("read", "write"), label=_("View and change")),
|
|
||||||
],
|
|
||||||
help_text=_("Some help text")
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(register_organizer_permission_groups)
|
|
||||||
def register_plugin_organizer_permissions(sender, **kwargs):
|
|
||||||
return [
|
|
||||||
PermissionGroup(
|
|
||||||
name="pretix_myplugin.resource",
|
|
||||||
label=_("Resources"),
|
|
||||||
actions=["read", "write"],
|
|
||||||
options=[
|
|
||||||
PermissionOption(actions=tuple(), label=_("No access")),
|
|
||||||
PermissionOption(actions=("read",), label=_("View")),
|
|
||||||
PermissionOption(actions=("read", "write"), label=_("View and change")),
|
|
||||||
],
|
|
||||||
help_text=_("Some help text")
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
.. _configuring teams and permissions: https://docs.pretix.eu/guides/teams/
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 54 KiB |
@@ -23,7 +23,6 @@ partition "For every cart position" {
|
|||||||
--> "Store as line_price (gross), tax_rate"
|
--> "Store as line_price (gross), tax_rate"
|
||||||
}
|
}
|
||||||
--> "Apply discount engine"
|
--> "Apply discount engine"
|
||||||
--> "Apply tax rounding"
|
|
||||||
--> "Store as price (gross)"
|
--> "Store as price (gross)"
|
||||||
|
|
||||||
@enduml
|
@enduml
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
sphinx==9.1.*
|
sphinx==7.4.*
|
||||||
sphinx-rtd-theme~=3.1.0
|
jinja2==3.1.*
|
||||||
sphinxcontrib-httpdomain~=2.0.0
|
sphinx-rtd-theme
|
||||||
sphinxcontrib-images~=1.0.1
|
sphinxcontrib-httpdomain
|
||||||
sphinxcontrib-jquery~=4.1
|
sphinxcontrib-images
|
||||||
sphinxcontrib-spelling~=8.0.2
|
sphinxcontrib-jquery
|
||||||
sphinxemoji~=0.3.2
|
sphinxcontrib-spelling==8.*
|
||||||
pyenchant==3.3.*
|
sphinxemoji
|
||||||
|
pyenchant==3.2.*
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
-e ../
|
-e ../
|
||||||
sphinx==9.1.*
|
sphinx==7.4.*
|
||||||
sphinx-rtd-theme~=3.1.0
|
jinja2==3.1.*
|
||||||
sphinxcontrib-httpdomain~=2.0.0
|
sphinx-rtd-theme
|
||||||
sphinxcontrib-images~=1.0.1
|
sphinxcontrib-httpdomain
|
||||||
sphinxcontrib-jquery~=4.1
|
sphinxcontrib-images
|
||||||
sphinxcontrib-spelling~=8.0.2
|
sphinxcontrib-jquery
|
||||||
sphinxemoji~=0.3.2
|
sphinxcontrib-spelling==8.*
|
||||||
pyenchant==3.3.*
|
sphinxemoji
|
||||||
|
pyenchant==3.2.*
|
||||||
|
|||||||
@@ -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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ coverage:
|
|||||||
coverage run -m py.test
|
coverage run -m py.test
|
||||||
|
|
||||||
npminstall:
|
npminstall:
|
||||||
# keep this in sync with pretix/_build.py!
|
# keep this in sync with setup.py!
|
||||||
mkdir -p pretix/static.dist/node_prefix/
|
mkdir -p pretix/static.dist/node_prefix/
|
||||||
cp -r pretix/static/npm_dir/* pretix/static.dist/node_prefix/
|
cp -r pretix/static/npm_dir/* pretix/static.dist/node_prefix/
|
||||||
npm ci --prefix=pretix/static.dist/node_prefix
|
npm install --prefix=pretix/static.dist/node_prefix
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -19,4 +19,4 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2026.4.0.dev0"
|
__version__ = "2025.8.0.dev0"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -39,7 +39,7 @@ def npm_install():
|
|||||||
node_prefix = os.path.join(here, 'static.dist', 'node_prefix')
|
node_prefix = os.path.join(here, 'static.dist', 'node_prefix')
|
||||||
os.makedirs(node_prefix, exist_ok=True)
|
os.makedirs(node_prefix, exist_ok=True)
|
||||||
shutil.copytree(os.path.join(here, 'static', 'npm_dir'), node_prefix, dirs_exist_ok=True)
|
shutil.copytree(os.path.join(here, 'static', 'npm_dir'), node_prefix, dirs_exist_ok=True)
|
||||||
subprocess.check_call('npm ci', shell=True, cwd=node_prefix)
|
subprocess.check_call('npm install', shell=True, cwd=node_prefix)
|
||||||
npm_installed = True
|
npm_installed = True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -36,9 +36,7 @@ from rest_framework.permissions import SAFE_METHODS, BasePermission
|
|||||||
|
|
||||||
from pretix.api.models import OAuthAccessToken
|
from pretix.api.models import OAuthAccessToken
|
||||||
from pretix.base.models import Device, Event, User
|
from pretix.base.models import Device, Event, User
|
||||||
from pretix.base.models.auth import (
|
from pretix.base.models.auth import SuperuserPermissionSet
|
||||||
EventPermissionSet, OrganizerPermissionSet, SuperuserPermissionSet,
|
|
||||||
)
|
|
||||||
from pretix.base.models.organizer import TeamAPIToken
|
from pretix.base.models.organizer import TeamAPIToken
|
||||||
from pretix.helpers.security import (
|
from pretix.helpers.security import (
|
||||||
Session2FASetupRequired, SessionInvalid, SessionPasswordChangeRequired,
|
Session2FASetupRequired, SessionInvalid, SessionPasswordChangeRequired,
|
||||||
@@ -87,7 +85,7 @@ class EventPermission(BasePermission):
|
|||||||
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
|
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
|
||||||
request.eventpermset = SuperuserPermissionSet()
|
request.eventpermset = SuperuserPermissionSet()
|
||||||
else:
|
else:
|
||||||
request.eventpermset = EventPermissionSet(perm_holder.get_event_permission_set(request.organizer, request.event))
|
request.eventpermset = perm_holder.get_event_permission_set(request.organizer, request.event)
|
||||||
|
|
||||||
if isinstance(required_permission, (list, tuple)):
|
if isinstance(required_permission, (list, tuple)):
|
||||||
if not any(p in request.eventpermset for p in required_permission):
|
if not any(p in request.eventpermset for p in required_permission):
|
||||||
@@ -102,7 +100,7 @@ class EventPermission(BasePermission):
|
|||||||
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
|
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
|
||||||
request.orgapermset = SuperuserPermissionSet()
|
request.orgapermset = SuperuserPermissionSet()
|
||||||
else:
|
else:
|
||||||
request.orgapermset = OrganizerPermissionSet(perm_holder.get_organizer_permission_set(request.organizer))
|
request.orgapermset = perm_holder.get_organizer_permission_set(request.organizer)
|
||||||
|
|
||||||
if isinstance(required_permission, (list, tuple)):
|
if isinstance(required_permission, (list, tuple)):
|
||||||
if not any(p in request.eventpermset for p in required_permission):
|
if not any(p in request.eventpermset for p in required_permission):
|
||||||
@@ -126,12 +124,12 @@ class EventCRUDPermission(EventPermission):
|
|||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
if not super(EventCRUDPermission, self).has_permission(request, view):
|
if not super(EventCRUDPermission, self).has_permission(request, view):
|
||||||
return False
|
return False
|
||||||
elif view.action == 'create' and 'organizer.events:create' not in request.orgapermset:
|
elif view.action == 'create' and 'can_create_events' not in request.orgapermset:
|
||||||
return False
|
return False
|
||||||
elif view.action == 'destroy' and 'event.settings.general:write' not in request.eventpermset:
|
elif view.action == 'destroy' and 'can_change_event_settings' not in request.eventpermset:
|
||||||
return False
|
return False
|
||||||
elif view.action in ['update', 'partial_update'] \
|
elif view.action in ['update', 'partial_update'] \
|
||||||
and 'event.settings.general:write' not in request.eventpermset:
|
and 'can_change_event_settings' not in request.eventpermset:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
# Generated by Django 4.2.24 on 2025-11-14 16:21
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("pretixapi", "0013_alter_webhookcallretry_retry_not_before"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="webhook",
|
|
||||||
name="target_url",
|
|
||||||
field=models.URLField(max_length=1024),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="webhookcall",
|
|
||||||
name="target_url",
|
|
||||||
field=models.URLField(max_length=1024),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -114,7 +114,7 @@ class OAuthRefreshToken(AbstractRefreshToken):
|
|||||||
class WebHook(models.Model):
|
class WebHook(models.Model):
|
||||||
organizer = models.ForeignKey('pretixbase.Organizer', on_delete=models.CASCADE, related_name='webhooks')
|
organizer = models.ForeignKey('pretixbase.Organizer', on_delete=models.CASCADE, related_name='webhooks')
|
||||||
enabled = models.BooleanField(default=True, verbose_name=_("Enable webhook"))
|
enabled = models.BooleanField(default=True, verbose_name=_("Enable webhook"))
|
||||||
target_url = models.URLField(verbose_name=_("Target URL"), max_length=1024)
|
target_url = models.URLField(verbose_name=_("Target URL"), max_length=255)
|
||||||
all_events = models.BooleanField(default=True, verbose_name=_("All events (including newly created ones)"))
|
all_events = models.BooleanField(default=True, verbose_name=_("All events (including newly created ones)"))
|
||||||
limit_events = models.ManyToManyField('pretixbase.Event', verbose_name=_("Limit to events"), blank=True)
|
limit_events = models.ManyToManyField('pretixbase.Event', verbose_name=_("Limit to events"), blank=True)
|
||||||
comment = models.CharField(verbose_name=_("Comment"), max_length=255, null=True, blank=True)
|
comment = models.CharField(verbose_name=_("Comment"), max_length=255, null=True, blank=True)
|
||||||
@@ -140,7 +140,7 @@ class WebHookEventListener(models.Model):
|
|||||||
class WebHookCall(models.Model):
|
class WebHookCall(models.Model):
|
||||||
webhook = models.ForeignKey('WebHook', on_delete=models.CASCADE, related_name='calls')
|
webhook = models.ForeignKey('WebHook', on_delete=models.CASCADE, related_name='calls')
|
||||||
datetime = models.DateTimeField(auto_now_add=True)
|
datetime = models.DateTimeField(auto_now_add=True)
|
||||||
target_url = models.URLField(max_length=1024)
|
target_url = models.URLField(max_length=255)
|
||||||
action_type = models.CharField(max_length=255)
|
action_type = models.CharField(max_length=255)
|
||||||
is_retry = models.BooleanField(default=False)
|
is_retry = models.BooleanField(default=False)
|
||||||
execution_time = models.FloatField(null=True)
|
execution_time = models.FloatField(null=True)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -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}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -47,9 +47,8 @@ from pretix.api.serializers.event import MetaDataField
|
|||||||
from pretix.api.serializers.fields import UploadedFileField
|
from pretix.api.serializers.fields import UploadedFileField
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemProgramTime,
|
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
|
||||||
ItemVariation, ItemVariationMetaValue, Question, QuestionOption, Quota,
|
ItemVariationMetaValue, Question, QuestionOption, Quota, SalesChannel,
|
||||||
SalesChannel,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -188,12 +187,6 @@ class InlineItemAddOnSerializer(serializers.ModelSerializer):
|
|||||||
'position', 'price_included', 'multi_allowed')
|
'position', 'price_included', 'multi_allowed')
|
||||||
|
|
||||||
|
|
||||||
class InlineItemProgramTimeSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = ItemProgramTime
|
|
||||||
fields = ('start', 'end')
|
|
||||||
|
|
||||||
|
|
||||||
class ItemBundleSerializer(serializers.ModelSerializer):
|
class ItemBundleSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ItemBundle
|
model = ItemBundle
|
||||||
@@ -219,37 +212,6 @@ class ItemBundleSerializer(serializers.ModelSerializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class ItemProgramTimeSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = ItemProgramTime
|
|
||||||
fields = ('id', 'start', 'end')
|
|
||||||
|
|
||||||
def validate(self, data):
|
|
||||||
data = super().validate(data)
|
|
||||||
|
|
||||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
|
||||||
full_data.update(data)
|
|
||||||
|
|
||||||
start = full_data.get('start')
|
|
||||||
if not start:
|
|
||||||
raise ValidationError(_("The program start must not be empty."))
|
|
||||||
|
|
||||||
end = full_data.get('end')
|
|
||||||
if not end:
|
|
||||||
raise ValidationError(_("The program end must not be empty."))
|
|
||||||
|
|
||||||
if start > end:
|
|
||||||
raise ValidationError(_("The program end must not be before the program start."))
|
|
||||||
|
|
||||||
event = self.context['event']
|
|
||||||
if event.has_subevents:
|
|
||||||
raise ValidationError({
|
|
||||||
_("You cannot use program times on an event series.")
|
|
||||||
})
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class ItemAddOnSerializer(serializers.ModelSerializer):
|
class ItemAddOnSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ItemAddOn
|
model = ItemAddOn
|
||||||
@@ -288,7 +250,6 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
addons = InlineItemAddOnSerializer(many=True, required=False)
|
addons = InlineItemAddOnSerializer(many=True, required=False)
|
||||||
bundles = InlineItemBundleSerializer(many=True, required=False)
|
bundles = InlineItemBundleSerializer(many=True, required=False)
|
||||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||||
program_times = InlineItemProgramTimeSerializer(many=True, required=False)
|
|
||||||
tax_rate = ItemTaxRateField(source='*', read_only=True)
|
tax_rate = ItemTaxRateField(source='*', read_only=True)
|
||||||
meta_data = MetaDataField(required=False, source='*')
|
meta_data = MetaDataField(required=False, source='*')
|
||||||
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
||||||
@@ -310,7 +271,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||||
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||||
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
|
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
|
||||||
'addons', 'bundles', 'program_times', 'original_price', 'require_approval', 'generate_tickets',
|
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
||||||
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'hidden_if_item_available_mode', 'allow_waitinglist',
|
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'hidden_if_item_available_mode', 'allow_waitinglist',
|
||||||
'issue_giftcard', 'meta_data',
|
'issue_giftcard', 'meta_data',
|
||||||
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
|
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
|
||||||
@@ -333,9 +294,9 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
if self.instance and ('addons' in data or 'variations' in data or 'bundles' in data or 'program_times' in data):
|
if self.instance and ('addons' in data or 'variations' in data or 'bundles' in data):
|
||||||
raise ValidationError(_('Updating add-ons, bundles, program times or variations via PATCH/PUT is not '
|
raise ValidationError(_('Updating add-ons, bundles, or variations via PATCH/PUT is not supported. Please use the '
|
||||||
'supported. Please use the dedicated nested endpoint.'))
|
'dedicated nested endpoint.'))
|
||||||
|
|
||||||
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
|
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
|
||||||
Item.clean_available(data.get('available_from'), data.get('available_until'))
|
Item.clean_available(data.get('available_from'), data.get('available_until'))
|
||||||
@@ -386,13 +347,6 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
ItemAddOn.clean_max_min_count(addon_data.get('max_count', 0), addon_data.get('min_count', 0))
|
ItemAddOn.clean_max_min_count(addon_data.get('max_count', 0), addon_data.get('min_count', 0))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def validate_program_times(self, value):
|
|
||||||
if not self.instance:
|
|
||||||
for program_time_data in value:
|
|
||||||
ItemProgramTime.clean_start_end(self, start=program_time_data.get('start', None),
|
|
||||||
end=program_time_data.get('end', None))
|
|
||||||
return value
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def item_meta_properties(self):
|
def item_meta_properties(self):
|
||||||
return {
|
return {
|
||||||
@@ -410,7 +364,6 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
variations_data = validated_data.pop('variations') if 'variations' in validated_data else {}
|
variations_data = validated_data.pop('variations') if 'variations' in validated_data else {}
|
||||||
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
|
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
|
||||||
bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {}
|
bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {}
|
||||||
program_times_data = validated_data.pop('program_times') if 'program_times' in validated_data else {}
|
|
||||||
meta_data = validated_data.pop('meta_data', None)
|
meta_data = validated_data.pop('meta_data', None)
|
||||||
picture = validated_data.pop('picture', None)
|
picture = validated_data.pop('picture', None)
|
||||||
require_membership_types = validated_data.pop('require_membership_types', [])
|
require_membership_types = validated_data.pop('require_membership_types', [])
|
||||||
@@ -445,8 +398,6 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
|||||||
ItemAddOn.objects.create(base_item=item, **addon_data)
|
ItemAddOn.objects.create(base_item=item, **addon_data)
|
||||||
for bundle_data in bundles_data:
|
for bundle_data in bundles_data:
|
||||||
ItemBundle.objects.create(base_item=item, **bundle_data)
|
ItemBundle.objects.create(base_item=item, **bundle_data)
|
||||||
for program_time_data in program_times_data:
|
|
||||||
ItemProgramTime.objects.create(item=item, **program_time_data)
|
|
||||||
|
|
||||||
# Meta data
|
# Meta data
|
||||||
if meta_data is not None:
|
if meta_data is not None:
|
||||||
@@ -599,7 +550,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
|||||||
if full_data.get('show_during_checkin') and full_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
|
if full_data.get('show_during_checkin') and full_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
|
||||||
raise ValidationError(_('This type of question cannot be shown during check-in.'))
|
raise ValidationError(_('This type of question cannot be shown during check-in.'))
|
||||||
|
|
||||||
Question.clean_items(event, full_data.get('items') or [])
|
Question.clean_items(event, full_data.get('items'))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def validate_options(self, value):
|
def validate_options(self, value):
|
||||||
@@ -615,7 +566,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
options_data = validated_data.pop('options') if 'options' in validated_data else []
|
options_data = validated_data.pop('options') if 'options' in validated_data else []
|
||||||
items = validated_data.pop('items', [])
|
items = validated_data.pop('items')
|
||||||
|
|
||||||
question = Question.objects.create(**validated_data)
|
question = Question.objects.create(**validated_data)
|
||||||
question.items.set(items)
|
question.items.set(items)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -24,7 +24,7 @@ from decimal import Decimal
|
|||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.order import OrderPositionSerializer
|
from pretix.api.serializers.order import OrderPositionSerializer
|
||||||
@@ -66,9 +66,6 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if 'linked_giftcard' in self.context['request'].query_params.getlist('expand'):
|
if 'linked_giftcard' in self.context['request'].query_params.getlist('expand'):
|
||||||
if not self.context["can_read_giftcards"]:
|
|
||||||
raise PermissionDenied("No permission to access gift card details.")
|
|
||||||
|
|
||||||
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True, context=self.context)
|
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True, context=self.context)
|
||||||
if 'linked_giftcard.owner_ticket' in self.context['request'].query_params.getlist('expand'):
|
if 'linked_giftcard.owner_ticket' in self.context['request'].query_params.getlist('expand'):
|
||||||
self.fields['linked_giftcard'].fields['owner_ticket'] = NestedOrderPositionSerializer(read_only=True, context=self.context)
|
self.fields['linked_giftcard'].fields['owner_ticket'] = NestedOrderPositionSerializer(read_only=True, context=self.context)
|
||||||
@@ -80,8 +77,6 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'):
|
if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'):
|
||||||
# No additional permission check performed, documented limitation of the permission system
|
|
||||||
# Would get to complex/unusable otherwise since the permission depends on the event
|
|
||||||
self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True)
|
self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True)
|
||||||
else:
|
else:
|
||||||
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
|
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
|
||||||
@@ -91,9 +86,6 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if 'customer' in self.context['request'].query_params.getlist('expand'):
|
if 'customer' in self.context['request'].query_params.getlist('expand'):
|
||||||
if not self.context["can_read_customers"]:
|
|
||||||
raise PermissionDenied("No permission to access customer details.")
|
|
||||||
|
|
||||||
self.fields['customer'] = CustomerSerializer(read_only=True)
|
self.fields['customer'] = CustomerSerializer(read_only=True)
|
||||||
else:
|
else:
|
||||||
self.fields['customer'] = serializers.SlugRelatedField(
|
self.fields['customer'] = serializers.SlugRelatedField(
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -19,7 +19,6 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from collections import Counter, defaultdict
|
from collections import Counter, defaultdict
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -37,8 +37,6 @@ logger = logging.getLogger(__name__)
|
|||||||
class SettingsSerializer(serializers.Serializer):
|
class SettingsSerializer(serializers.Serializer):
|
||||||
default_fields = []
|
default_fields = []
|
||||||
readonly_fields = []
|
readonly_fields = []
|
||||||
default_write_permission = 'organizer.settings.general:write'
|
|
||||||
write_permission_required = {}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.changed_data = []
|
self.changed_data = []
|
||||||
@@ -60,17 +58,9 @@ class SettingsSerializer(serializers.Serializer):
|
|||||||
f._label = str(form_kwargs.get('label', fname))
|
f._label = str(form_kwargs.get('label', fname))
|
||||||
f._help_text = str(form_kwargs.get('help_text'))
|
f._help_text = str(form_kwargs.get('help_text'))
|
||||||
f.parent = self
|
f.parent = self
|
||||||
|
|
||||||
self.write_permission_required[fname] = DEFAULTS[fname].get('write_permission', self.default_write_permission)
|
|
||||||
|
|
||||||
self.fields[fname] = f
|
self.fields[fname] = f
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
for k in attrs.keys():
|
|
||||||
p = self.write_permission_required.get(k, self.default_write_permission)
|
|
||||||
if p not in self.context["permissions"]:
|
|
||||||
raise ValidationError({k: f"Setting this field requires permission {p}"})
|
|
||||||
|
|
||||||
return {k: v for k, v in attrs.items() if k not in self.readonly_fields}
|
return {k: v for k, v in attrs.items() if k not in self.readonly_fields}
|
||||||
|
|
||||||
def update(self, instance: HierarkeyProxy, validated_data):
|
def update(self, instance: HierarkeyProxy, validated_data):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -52,8 +52,8 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
|
|||||||
ordering = ('datetime',)
|
ordering = ('datetime',)
|
||||||
ordering_fields = ('datetime', 'cart_id')
|
ordering_fields = ('datetime', 'cart_id')
|
||||||
lookup_field = 'id'
|
lookup_field = 'id'
|
||||||
permission = 'event.orders:read'
|
permission = 'can_view_orders'
|
||||||
write_permission = 'event.orders:write'
|
write_permission = 'can_change_orders'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return CartPosition.objects.filter(
|
return CartPosition.objects.filter(
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -57,7 +57,7 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('position', 'id')
|
ordering = ('position', 'id')
|
||||||
permission = None
|
permission = None
|
||||||
write_permission = 'event.items:write'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.discounts.prefetch_related(
|
return self.request.event.discounts.prefetch_related(
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -281,11 +281,6 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
new_event = serializer.save(organizer=self.request.organizer)
|
new_event = serializer.save(organizer=self.request.organizer)
|
||||||
|
|
||||||
if copy_from:
|
if copy_from:
|
||||||
perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken))
|
|
||||||
else self.request.user)
|
|
||||||
if not copy_from.allow_copy_data(self.request.organizer, perm_holder):
|
|
||||||
raise PermissionDenied("Not sufficient permission on source event to copy")
|
|
||||||
|
|
||||||
new_event.copy_data_from(copy_from, skip_meta_data='meta_data' in serializer.validated_data)
|
new_event.copy_data_from(copy_from, skip_meta_data='meta_data' in serializer.validated_data)
|
||||||
|
|
||||||
if plugins is not None:
|
if plugins is not None:
|
||||||
@@ -346,24 +341,15 @@ class CloneEventViewSet(viewsets.ModelViewSet):
|
|||||||
lookup_field = 'slug'
|
lookup_field = 'slug'
|
||||||
lookup_url_kwarg = 'event'
|
lookup_url_kwarg = 'event'
|
||||||
http_method_names = ['post']
|
http_method_names = ['post']
|
||||||
write_permission = 'event.settings.general:write'
|
write_permission = 'can_create_events'
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
ctx['event'] = Event.objects.get(slug=self.kwargs['event'], organizer=self.request.organizer)
|
ctx['event'] = self.kwargs['event']
|
||||||
ctx['organizer'] = self.request.organizer
|
ctx['organizer'] = self.request.organizer
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
# Weird edge case: Requires settings permission on the event (to read) but also on the organizer (two write)
|
|
||||||
perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken))
|
|
||||||
else self.request.user)
|
|
||||||
if not perm_holder.has_organizer_permission(self.request.organizer, "organizer.events:create", request=self.request):
|
|
||||||
raise PermissionDenied("No permission to create events")
|
|
||||||
|
|
||||||
if not serializer.context['event'].allow_copy_data(self.request.organizer, perm_holder):
|
|
||||||
raise PermissionDenied("Not sufficient permission on source event to copy")
|
|
||||||
|
|
||||||
serializer.save(organizer=self.request.organizer)
|
serializer.save(organizer=self.request.organizer)
|
||||||
|
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
@@ -440,7 +426,7 @@ with scopes_disabled():
|
|||||||
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = SubEventSerializer
|
serializer_class = SubEventSerializer
|
||||||
queryset = SubEvent.objects.none()
|
queryset = SubEvent.objects.none()
|
||||||
write_permission = 'event.subevents:write'
|
write_permission = 'can_change_event_settings'
|
||||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
||||||
ordering = ('date_from',)
|
ordering = ('date_from',)
|
||||||
ordering_fields = ('id', 'date_from', 'last_modified')
|
ordering_fields = ('id', 'date_from', 'last_modified')
|
||||||
@@ -560,7 +546,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = TaxRuleSerializer
|
serializer_class = TaxRuleSerializer
|
||||||
queryset = TaxRule.objects.none()
|
queryset = TaxRule.objects.none()
|
||||||
write_permission = 'event.settings.tax:write'
|
write_permission = 'can_change_event_settings'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.tax_rules.all()
|
return self.request.event.tax_rules.all()
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -40,12 +40,12 @@ from pretix.api.serializers.exporters import (
|
|||||||
)
|
)
|
||||||
from pretix.base.exporter import OrganizerLevelExportMixin
|
from pretix.base.exporter import OrganizerLevelExportMixin
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedFile, Device, ScheduledEventExport, ScheduledOrganizerExport,
|
CachedFile, Device, Event, ScheduledEventExport, ScheduledOrganizerExport,
|
||||||
TeamAPIToken,
|
TeamAPIToken,
|
||||||
)
|
)
|
||||||
from pretix.base.models.organizer import TeamQuerySet
|
from pretix.base.services.export import export, multiexport
|
||||||
from pretix.base.services.export import (
|
from pretix.base.signals import (
|
||||||
export, init_event_exporters, init_organizer_exporters, multiexport,
|
register_data_exporters, register_multievent_data_exporters,
|
||||||
)
|
)
|
||||||
from pretix.helpers.http import ChunkBasedFileResponse
|
from pretix.helpers.http import ChunkBasedFileResponse
|
||||||
|
|
||||||
@@ -74,11 +74,6 @@ class ExportersMixin:
|
|||||||
@action(detail=True, methods=['GET'], url_name='download', url_path='download/(?P<asyncid>[^/]+)/(?P<cfid>[^/]+)')
|
@action(detail=True, methods=['GET'], url_name='download', url_path='download/(?P<asyncid>[^/]+)/(?P<cfid>[^/]+)')
|
||||||
def download(self, *args, **kwargs):
|
def download(self, *args, **kwargs):
|
||||||
cf = get_object_or_404(CachedFile, id=kwargs['cfid'])
|
cf = get_object_or_404(CachedFile, id=kwargs['cfid'])
|
||||||
if not cf.allowed_for_session(self.request, "exporters-api"):
|
|
||||||
return Response(
|
|
||||||
{'status': 'failed', 'message': 'Unknown file ID or export failed'},
|
|
||||||
status=status.HTTP_410_GONE
|
|
||||||
)
|
|
||||||
if cf.file:
|
if cf.file:
|
||||||
resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type)
|
resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type)
|
||||||
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename).encode("ascii", "ignore")
|
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename).encode("ascii", "ignore")
|
||||||
@@ -111,11 +106,10 @@ class ExportersMixin:
|
|||||||
@action(detail=True, methods=['POST'])
|
@action(detail=True, methods=['POST'])
|
||||||
def run(self, *args, **kwargs):
|
def run(self, *args, **kwargs):
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
serializer = JobRunSerializer(exporter=instance, data=self.request.data)
|
serializer = JobRunSerializer(exporter=instance, data=self.request.data, **self.get_serializer_kwargs())
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
cf = CachedFile(web_download=True)
|
cf = CachedFile(web_download=False)
|
||||||
cf.bind_to_session(self.request, "exporters-api")
|
|
||||||
cf.date = now()
|
cf.date = now()
|
||||||
cf.expires = now() + timedelta(hours=24)
|
cf.expires = now() + timedelta(hours=24)
|
||||||
cf.save()
|
cf.save()
|
||||||
@@ -136,34 +130,27 @@ class ExportersMixin:
|
|||||||
|
|
||||||
|
|
||||||
class EventExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
class EventExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
||||||
permission = None
|
permission = 'can_view_orders'
|
||||||
|
|
||||||
|
def get_serializer_kwargs(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def exporters(self):
|
def exporters(self):
|
||||||
raw_exporters = list(init_event_exporters(
|
|
||||||
event=self.request.event,
|
|
||||||
user=self.request.user if self.request.user and self.request.user.is_authenticated else None,
|
|
||||||
token=self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None,
|
|
||||||
device=self.request.auth if isinstance(self.request.auth, Device) else None,
|
|
||||||
request=self.request,
|
|
||||||
))
|
|
||||||
exporters = []
|
exporters = []
|
||||||
|
responses = register_data_exporters.send(self.request.event)
|
||||||
|
raw_exporters = [response(self.request.event, self.request.organizer) for r, response in responses if response]
|
||||||
|
raw_exporters = [
|
||||||
|
ex for ex in raw_exporters
|
||||||
|
if ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
|
||||||
|
]
|
||||||
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
|
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
|
||||||
ex._serializer = JobRunSerializer(exporter=ex)
|
ex._serializer = JobRunSerializer(exporter=ex)
|
||||||
exporters.append(ex)
|
exporters.append(ex)
|
||||||
return exporters
|
return exporters
|
||||||
|
|
||||||
def do_export(self, cf, instance, data):
|
def do_export(self, cf, instance, data):
|
||||||
return export.apply_async(args=(
|
return export.apply_async(args=(self.request.event.id, str(cf.id), instance.identifier, data))
|
||||||
self.request.event.id,
|
|
||||||
), kwargs={
|
|
||||||
'user': self.request.user.pk if self.request.user and self.request.user.is_authenticated else None,
|
|
||||||
'token': self.request.auth.pk if isinstance(self.request.auth, TeamAPIToken) else None,
|
|
||||||
'device': self.request.auth.pk if isinstance(self.request.auth, Device) else None,
|
|
||||||
'fileid': str(cf.id),
|
|
||||||
'provider': instance.identifier,
|
|
||||||
'form_data': data,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
||||||
@@ -171,23 +158,47 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def exporters(self):
|
def exporters(self):
|
||||||
raw_exporters = list(init_organizer_exporters(
|
|
||||||
organizer=self.request.organizer,
|
|
||||||
user=self.request.user if self.request.user and self.request.user.is_authenticated else None,
|
|
||||||
token=self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None,
|
|
||||||
device=self.request.auth if isinstance(self.request.auth, Device) else None,
|
|
||||||
request=self.request,
|
|
||||||
))
|
|
||||||
exporters = []
|
exporters = []
|
||||||
|
if isinstance(self.request.auth, (Device, TeamAPIToken)):
|
||||||
|
perm_holder = self.request.auth
|
||||||
|
else:
|
||||||
|
perm_holder = self.request.user
|
||||||
|
events = perm_holder.get_events_with_permission('can_view_orders', request=self.request).filter(
|
||||||
|
organizer=self.request.organizer
|
||||||
|
)
|
||||||
|
responses = register_multievent_data_exporters.send(self.request.organizer)
|
||||||
|
raw_exporters = [
|
||||||
|
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else events, self.request.organizer)
|
||||||
|
for r, response in responses
|
||||||
|
if response
|
||||||
|
]
|
||||||
|
raw_exporters = [
|
||||||
|
ex for ex in raw_exporters
|
||||||
|
if (
|
||||||
|
not isinstance(ex, OrganizerLevelExportMixin) or
|
||||||
|
perm_holder.has_organizer_permission(self.request.organizer, ex.organizer_required_permission, self.request)
|
||||||
|
) and ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
|
||||||
|
]
|
||||||
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
|
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
|
||||||
ex._serializer = JobRunSerializer(exporter=ex)
|
ex._serializer = JobRunSerializer(exporter=ex, events=events)
|
||||||
exporters.append(ex)
|
exporters.append(ex)
|
||||||
return exporters
|
return exporters
|
||||||
|
|
||||||
|
def get_serializer_kwargs(self):
|
||||||
|
if isinstance(self.request.auth, (Device, TeamAPIToken)):
|
||||||
|
perm_holder = self.request.auth
|
||||||
|
else:
|
||||||
|
perm_holder = self.request.user
|
||||||
|
return {
|
||||||
|
'events': perm_holder.get_events_with_permission('can_view_orders', request=self.request).filter(
|
||||||
|
organizer=self.request.organizer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
def do_export(self, cf, instance, data):
|
def do_export(self, cf, instance, data):
|
||||||
return multiexport.apply_async(kwargs={
|
return multiexport.apply_async(kwargs={
|
||||||
'organizer': self.request.organizer.id,
|
'organizer': self.request.organizer.id,
|
||||||
'user': self.request.user.id if self.request.user and self.request.user.is_authenticated else None,
|
'user': self.request.user.id if self.request.user.is_authenticated else None,
|
||||||
'token': self.request.auth.pk if isinstance(self.request.auth, TeamAPIToken) else None,
|
'token': self.request.auth.pk if isinstance(self.request.auth, TeamAPIToken) else None,
|
||||||
'device': self.request.auth.pk if isinstance(self.request.auth, Device) else None,
|
'device': self.request.auth.pk if isinstance(self.request.auth, Device) else None,
|
||||||
'fileid': str(cf.id),
|
'fileid': str(cf.id),
|
||||||
@@ -205,11 +216,11 @@ class ScheduledExportersViewSet(viewsets.ModelViewSet):
|
|||||||
class ScheduledEventExportViewSet(ScheduledExportersViewSet):
|
class ScheduledEventExportViewSet(ScheduledExportersViewSet):
|
||||||
serializer_class = ScheduledEventExportSerializer
|
serializer_class = ScheduledEventExportSerializer
|
||||||
queryset = ScheduledEventExport.objects.none()
|
queryset = ScheduledEventExport.objects.none()
|
||||||
permission = None
|
permission = 'can_view_orders'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
|
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
|
||||||
if not perm_holder.has_event_permission(self.request.organizer, self.request.event, 'event.settings.general:write',
|
if not perm_holder.has_event_permission(self.request.organizer, self.request.event, 'can_change_event_settings',
|
||||||
request=self.request):
|
request=self.request):
|
||||||
if self.request.user.is_authenticated:
|
if self.request.user.is_authenticated:
|
||||||
qs = self.request.event.scheduled_exports.filter(owner=self.request.user)
|
qs = self.request.event.scheduled_exports.filter(owner=self.request.user)
|
||||||
@@ -241,28 +252,11 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def exporters(self):
|
def exporters(self):
|
||||||
exporters = list(init_event_exporters(
|
responses = register_data_exporters.send(self.request.event)
|
||||||
event=self.request.event,
|
exporters = [response(self.request.event, self.request.organizer) for r, response in responses if response]
|
||||||
user=self.request.user if self.request.user and self.request.user.is_authenticated else None,
|
|
||||||
token=self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None,
|
|
||||||
device=self.request.auth if isinstance(self.request.auth, Device) else None,
|
|
||||||
request=self.request,
|
|
||||||
))
|
|
||||||
return {e.identifier: e for e in exporters}
|
return {e.identifier: e for e in exporters}
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
if not self.request.user.is_authenticated or self.request.user != serializer.instance.owner:
|
|
||||||
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
|
|
||||||
# user B has settings permission (= they can see the export configuration), but not enough permission
|
|
||||||
# to run the export themselves. Without this check, user B could modify the export and add themselves
|
|
||||||
# as a recipient. Thereby, user B would gain access to data they can't have.
|
|
||||||
exporter = self.exporters.get(serializer.instance.export_identifier)
|
|
||||||
if not exporter:
|
|
||||||
raise PermissionDenied("No access to exporter.")
|
|
||||||
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
|
|
||||||
if not perm_holder.has_event_permission(self.request.organizer, self.request.event, exporter.get_required_event_permission()):
|
|
||||||
raise PermissionDenied("No permission to edit exports you could not run.")
|
|
||||||
|
|
||||||
serializer.save(event=self.request.event)
|
serializer.save(event=self.request.event)
|
||||||
serializer.instance.compute_next_run()
|
serializer.instance.compute_next_run()
|
||||||
serializer.instance.error_counter = 0
|
serializer.instance.error_counter = 0
|
||||||
@@ -291,7 +285,7 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
|
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
|
||||||
if not perm_holder.has_organizer_permission(self.request.organizer, 'organizer.settings.general:write',
|
if not perm_holder.has_organizer_permission(self.request.organizer, 'can_change_organizer_settings',
|
||||||
request=self.request):
|
request=self.request):
|
||||||
if self.request.user.is_authenticated:
|
if self.request.user.is_authenticated:
|
||||||
qs = self.request.organizer.scheduled_exports.filter(owner=self.request.user)
|
qs = self.request.organizer.scheduled_exports.filter(owner=self.request.user)
|
||||||
@@ -321,55 +315,26 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
|
|||||||
ctx['exporters'] = self.exporters
|
ctx['exporters'] = self.exporters
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def events(self):
|
||||||
|
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||||
|
return self.request.auth.get_events_with_permission('can_view_orders')
|
||||||
|
elif self.request.user.is_authenticated:
|
||||||
|
return self.request.user.get_events_with_permission('can_view_orders', self.request).filter(
|
||||||
|
organizer=self.request.organizer
|
||||||
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def exporters(self):
|
def exporters(self):
|
||||||
exporters = list(init_organizer_exporters(
|
responses = register_multievent_data_exporters.send(self.request.organizer)
|
||||||
organizer=self.request.organizer,
|
exporters = [
|
||||||
user=self.request.user if self.request.user and self.request.user.is_authenticated else None,
|
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else self.events,
|
||||||
token=self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None,
|
self.request.organizer)
|
||||||
device=self.request.auth if isinstance(self.request.auth, Device) else None,
|
for r, response in responses if response
|
||||||
request=self.request,
|
]
|
||||||
))
|
|
||||||
return {e.identifier: e for e in exporters}
|
return {e.identifier: e for e in exporters}
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
if not self.request.user.is_authenticated or self.request.user != serializer.instance.owner:
|
|
||||||
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
|
|
||||||
# user B has settings permission (= they can see the export configuration), but not enough permission
|
|
||||||
# to run the export themselves. Without this check, user B could modify the export and add themselves
|
|
||||||
# as a recipient. Thereby, user B would gain access to data they can't have.
|
|
||||||
exporter = self.exporters.get(serializer.instance.export_identifier)
|
|
||||||
if not exporter:
|
|
||||||
raise PermissionDenied("No access to exporter.")
|
|
||||||
perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken))
|
|
||||||
else self.request.user)
|
|
||||||
if isinstance(exporter, OrganizerLevelExportMixin):
|
|
||||||
if not perm_holder.has_organizer_permission(
|
|
||||||
self.request.organizer, exporter.get_required_organizer_permission(), request=self.request,
|
|
||||||
):
|
|
||||||
raise PermissionDenied("No permission to edit exports you could not run.")
|
|
||||||
else:
|
|
||||||
if serializer.instance.export_form_data.get("all_events", False):
|
|
||||||
if isinstance(self.request.auth, Device):
|
|
||||||
if not self.request.auth.all_events:
|
|
||||||
raise PermissionDenied("No permission to edit exports you could not run.")
|
|
||||||
elif isinstance(self.request.auth, TeamAPIToken):
|
|
||||||
if not self.request.auth.team.all_events:
|
|
||||||
raise PermissionDenied("No permission to edit exports you could not run.")
|
|
||||||
elif self.request.user.is_authenticated:
|
|
||||||
if not self.request.user.teams.filter(
|
|
||||||
TeamQuerySet.event_permission_q(exporter.get_required_event_permission()),
|
|
||||||
all_events=True,
|
|
||||||
).exists():
|
|
||||||
raise PermissionDenied("No permission to edit exports you could not run.")
|
|
||||||
else:
|
|
||||||
events_selected = serializer.instance.export_form_data.get("events", [])
|
|
||||||
events_permission = set(perm_holder.get_events_with_permission(
|
|
||||||
exporter.get_required_event_permission(), request=self.request
|
|
||||||
).values_list("pk", flat=True))
|
|
||||||
if not all(e in events_permission for e in events_selected):
|
|
||||||
raise PermissionDenied("No permission to edit exports you could not run.")
|
|
||||||
|
|
||||||
serializer.save(organizer=self.request.organizer)
|
serializer.save(organizer=self.request.organizer)
|
||||||
serializer.instance.compute_next_run()
|
serializer.instance.compute_next_run()
|
||||||
serializer.instance.error_counter = 0
|
serializer.instance.error_counter = 0
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -40,19 +40,19 @@ from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
|||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from pretix.api.pagination import TotalOrderingFilter
|
from pretix.api.pagination import TotalOrderingFilter
|
||||||
from pretix.api.serializers.item import (
|
from pretix.api.serializers.item import (
|
||||||
ItemAddOnSerializer, ItemBundleSerializer, ItemCategorySerializer,
|
ItemAddOnSerializer, ItemBundleSerializer, ItemCategorySerializer,
|
||||||
ItemProgramTimeSerializer, ItemSerializer, ItemVariationSerializer,
|
ItemSerializer, ItemVariationSerializer, QuestionOptionSerializer,
|
||||||
QuestionOptionSerializer, QuestionSerializer, QuotaSerializer,
|
QuestionSerializer, QuotaSerializer,
|
||||||
)
|
)
|
||||||
from pretix.api.views import ConditionalListView
|
from pretix.api.views import ConditionalListView
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemProgramTime,
|
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation,
|
||||||
ItemVariation, Question, QuestionOption, Quota,
|
Question, QuestionOption, Quota,
|
||||||
)
|
)
|
||||||
from pretix.base.services.quotas import QuotaAvailability
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
@@ -99,14 +99,14 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
ordering = ('position', 'id')
|
ordering = ('position', 'id')
|
||||||
filterset_class = ItemFilter
|
filterset_class = ItemFilter
|
||||||
permission = None
|
permission = None
|
||||||
write_permission = 'event.items:write'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.items.select_related('tax_rule').prefetch_related(
|
return self.request.event.items.select_related('tax_rule').prefetch_related(
|
||||||
'variations', 'addons', 'bundles', 'meta_values', 'meta_values__property',
|
'variations', 'addons', 'bundles', 'meta_values', 'meta_values__property',
|
||||||
'variations__meta_values', 'variations__meta_values__property',
|
'variations__meta_values', 'variations__meta_values__property',
|
||||||
'require_membership_types', 'variations__require_membership_types',
|
'require_membership_types', 'variations__require_membership_types',
|
||||||
'limit_sales_channels', 'variations__limit_sales_channels', 'program_times'
|
'limit_sales_channels', 'variations__limit_sales_channels',
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
@@ -163,7 +163,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
|||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('id',)
|
ordering = ('id',)
|
||||||
permission = None
|
permission = None
|
||||||
write_permission = 'event.items:write'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def item(self):
|
def item(self):
|
||||||
@@ -234,7 +234,7 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
|
|||||||
ordering_fields = ('id',)
|
ordering_fields = ('id',)
|
||||||
ordering = ('id',)
|
ordering = ('id',)
|
||||||
permission = None
|
permission = None
|
||||||
write_permission = 'event.items:write'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def item(self):
|
def item(self):
|
||||||
@@ -279,59 +279,6 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ItemProgramTimeViewSet(viewsets.ModelViewSet):
|
|
||||||
serializer_class = ItemProgramTimeSerializer
|
|
||||||
queryset = ItemProgramTime.objects.none()
|
|
||||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter,)
|
|
||||||
ordering_fields = ('id',)
|
|
||||||
ordering = ('id',)
|
|
||||||
permission = None
|
|
||||||
write_permission = 'event.items:write'
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def item(self):
|
|
||||||
return get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
if self.request.event.has_subevents:
|
|
||||||
raise ValidationError('You cannot use program times on an event series.')
|
|
||||||
return self.item.program_times.all()
|
|
||||||
|
|
||||||
def get_serializer_context(self):
|
|
||||||
ctx = super().get_serializer_context()
|
|
||||||
ctx['event'] = self.request.event
|
|
||||||
ctx['item'] = self.item
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
|
||||||
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
|
||||||
serializer.save(item=item)
|
|
||||||
item.log_action(
|
|
||||||
'pretix.event.item.program_times.added',
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
|
||||||
)
|
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
|
||||||
serializer.save(event=self.request.event)
|
|
||||||
serializer.instance.item.log_action(
|
|
||||||
'pretix.event.item.program_times.changed',
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
|
||||||
)
|
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
|
||||||
super().perform_destroy(instance)
|
|
||||||
instance.item.log_action(
|
|
||||||
'pretix.event.item.program_times.removed',
|
|
||||||
user=self.request.user,
|
|
||||||
auth=self.request.auth,
|
|
||||||
data={'start': instance.start, 'end': instance.end}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ItemAddOnViewSet(viewsets.ModelViewSet):
|
class ItemAddOnViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = ItemAddOnSerializer
|
serializer_class = ItemAddOnSerializer
|
||||||
queryset = ItemAddOn.objects.none()
|
queryset = ItemAddOn.objects.none()
|
||||||
@@ -339,7 +286,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
|||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('id',)
|
ordering = ('id',)
|
||||||
permission = None
|
permission = None
|
||||||
write_permission = 'event.items:write'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def item(self):
|
def item(self):
|
||||||
@@ -398,7 +345,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('position', 'id')
|
ordering = ('position', 'id')
|
||||||
permission = None
|
permission = None
|
||||||
write_permission = 'event.items:write'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.categories.all()
|
return self.request.event.categories.all()
|
||||||
@@ -453,7 +400,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('position', 'id')
|
ordering = ('position', 'id')
|
||||||
permission = None
|
permission = None
|
||||||
write_permission = 'event.items:write'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.questions.prefetch_related('options').all()
|
return self.request.event.questions.prefetch_related('options').all()
|
||||||
@@ -497,7 +444,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
|||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('position',)
|
ordering = ('position',)
|
||||||
permission = None
|
permission = None
|
||||||
write_permission = 'event.items:write'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
q = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
|
q = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
|
||||||
@@ -564,10 +511,10 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
ordering_fields = ('id', 'size')
|
ordering_fields = ('id', 'size')
|
||||||
ordering = ('id',)
|
ordering = ('id',)
|
||||||
permission = None
|
permission = None
|
||||||
write_permission = 'event.items:write'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.quotas.select_related('subevent').prefetch_related('items', 'variations').all()
|
return self.request.event.quotas.all()
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
queryset = self.filter_queryset(self.get_queryset()).distinct()
|
queryset = self.filter_queryset(self.get_queryset()).distinct()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -62,8 +62,8 @@ with scopes_disabled():
|
|||||||
class ReusableMediaViewSet(viewsets.ModelViewSet):
|
class ReusableMediaViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = ReusableMediaSerializer
|
serializer_class = ReusableMediaSerializer
|
||||||
queryset = ReusableMedium.objects.none()
|
queryset = ReusableMedium.objects.none()
|
||||||
permission = 'organizer.reusablemedia:read'
|
permission = 'can_manage_reusable_media'
|
||||||
write_permission = 'organizer.reusablemedia:write'
|
write_permission = 'can_manage_reusable_media'
|
||||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||||
ordering = ('-updated', '-id')
|
ordering = ('-updated', '-id')
|
||||||
ordering_fields = ('created', 'updated', 'identifier', 'type', 'id')
|
ordering_fields = ('created', 'updated', 'identifier', 'type', 'id')
|
||||||
@@ -95,8 +95,6 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
|
|||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
ctx['organizer'] = self.request.organizer
|
ctx['organizer'] = self.request.organizer
|
||||||
ctx['can_read_giftcards'] = 'organizer.giftcards:read' in self.request.orgapermset
|
|
||||||
ctx['can_read_customers'] = 'organizer.customers:read' in self.request.orgapermset
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@transaction.atomic()
|
@transaction.atomic()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -57,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")
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,),)
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user