mirror of
https://github.com/pretix/pretix.git
synced 2026-04-23 23:22:32 +00:00
Compare commits
1 Commits
master
...
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,16 +1,11 @@
|
|||||||
Contributing to pretix
|
Contributing to pretix
|
||||||
======================
|
======================
|
||||||
|
|
||||||
Welcome to pretix, we are happy that you would like to contribute.
|
Hey there and welcome to pretix!
|
||||||
Before you do so, please make sure to read the following documents:
|
|
||||||
|
|
||||||
- [Contribution workflow](https://docs.pretix.eu/dev/development/contribution/general.html)
|
* We've got a contributors guide in [our documentation](https://docs.pretix.eu/dev/development/contribution/) together with notes on the [development setup](https://docs.pretix.eu/dev/development/setup.html).
|
||||||
- [AI-assisted contribution policy](https://docs.pretix.eu/dev/development/contribution/ai.html)
|
|
||||||
- [Coding style and quality](https://docs.pretix.eu/dev/development/contribution/style.html)
|
|
||||||
- [Development setup](https://docs.pretix.eu/dev/development/setup.html)
|
|
||||||
- [Code of Conduct](https://docs.pretix.eu/dev/development/contribution/codeofconduct.html)
|
|
||||||
|
|
||||||
Before we can accept your first PR we'll need you to sign [our **Contributor License Agreement** (CLA)](https://pretix.eu/about/en/cla).
|
* Please note that we have a [Code of Conduct](https://docs.pretix.eu/dev/development/contribution/codeofconduct.html) in place that applies to all project contributions, including issues, pull requests, etc.
|
||||||
You can find more information about the how and why in our [License FAQ](https://docs.pretix.eu/trust/licensing/faq/) and in our [license change blog post](https://pretix.eu/about/en/blog/20210412-license/).
|
|
||||||
|
* Before we can accept a PR from you we'll need you to sign [our CLA](https://pretix.eu/about/en/cla). You can find more information about the how and why in our [License FAQ](https://docs.pretix.eu/trust/licensing/faq/) and in our [license change blog post](https://pretix.eu/about/en/blog/20210412-license/).
|
||||||
|
|
||||||
**Before contributing new functionality, always open a discussion first.**
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
.. _`aipolicy`:
|
|
||||||
|
|
||||||
AI-assisted contribution policy
|
|
||||||
===============================
|
|
||||||
|
|
||||||
pretix is maintained by humans.
|
|
||||||
Every discussion, issue, and pull request is read and reviewed by humans (and sometimes machines, too).
|
|
||||||
We ask you to respect the time and effort put in by these humans by not sending low-effort, unqualified work, since it puts the burden of validation on the maintainer.
|
|
||||||
|
|
||||||
Therefore, the pretix project has strict rules for AI usage:
|
|
||||||
|
|
||||||
- **All AI usage in any form must be disclosed.** You must state the tool you used (e.g. Claude Code, Cursor, Amp) along with the extent that the work was AI-assisted.
|
|
||||||
|
|
||||||
- **The human-in-the-loop must fully understand all code.** If you can't explain what your changes do and how they interact with the greater system without the aid of AI tools, do not contribute to this project.
|
|
||||||
|
|
||||||
- **Issues and discussions can use AI assistance but must have a full human-in-the-loop.** This means that any content generated with AI must have been reviewed and edited by a human before submission. AI is very good at being overly verbose and including noise that distracts from the main point. Humans must do their research and trim this down.
|
|
||||||
|
|
||||||
- **No AI-generated media is allowed (art, images, videos, audio, etc.).** Text and code are the only acceptable AI-generated content, per the other rules in this policy.
|
|
||||||
|
|
||||||
- **Bad AI drivers will be excluded from the project.** People who produce bad contributions that are clearly AI (slop) will be blocked from our organization without warning.
|
|
||||||
|
|
||||||
This policy was inspired by the `ghostty project`_.
|
|
||||||
|
|
||||||
.. _ghostty project: https://github.com/ghostty-org/ghostty/blob/main/AI_POLICY.md
|
|
||||||
@@ -1,39 +1,23 @@
|
|||||||
Contribution workflow
|
General remarks
|
||||||
=====================
|
===============
|
||||||
|
|
||||||
You are interested in contributing to pretix? That is awesome!
|
You are interested in contributing to pretix? That is awesome!
|
||||||
|
|
||||||
If you’re new to contributing to open source software, don’t be afraid. We’ll happily review your code and give you
|
If you’re new to contributing to open source software, don’t be afraid. We’ll happily review your code and give you
|
||||||
constructive and friendly feedback on your changes. Every contribution should go through the following steps.
|
constructive and friendly feedback on your changes.
|
||||||
|
|
||||||
Discussion & Design
|
First of all, you'll need pretix running locally on your machine. Head over to :ref:`devsetup` to learn how to do this.
|
||||||
-------------------
|
|
||||||
|
|
||||||
pretix is a large and mature project with more of a decade of history and hopefully many more decades to come.
|
|
||||||
Keeping pretix in good shape over long timeframes is first and foremost a fight against complexity.
|
|
||||||
With every additional feature, complexity grows, and both features and complexity are hard to remove.
|
|
||||||
|
|
||||||
Even if you are doing the initial work of the contribution, accepting the contribution is not free for us.
|
|
||||||
Not only will we need to maintain the feature, but every feature adds cost to the maintenance of every other feature it interacts with, and every feature adds effort for users to understand how pretix works.
|
|
||||||
Therefore, we must carefully select what features we add, based on how well they fit the system in general and of how much use they will be to our larger user base.
|
|
||||||
|
|
||||||
We strongly ask you to **create a discussion on GitHub for every new feature idea** outlining the use case and the proposed implementation design.
|
|
||||||
Pull requests without prior discussion will likely just be closed.
|
|
||||||
|
|
||||||
For bug fixes and very minor changes, you can skip this step and open a PR right away.
|
|
||||||
|
|
||||||
Development
|
|
||||||
-----------
|
|
||||||
|
|
||||||
To develop your contribution, you'll need pretix running locally on your machine. Head over to :ref:`devsetup` to learn how to do this.
|
|
||||||
If you run into any problems on your way, please do not hesitate to ask us anytime!
|
If you run into any problems on your way, please do not hesitate to ask us anytime!
|
||||||
|
|
||||||
While developing, please have a look at our :ref:`aipolicy` and our guidelines on :ref:`codestyle`.
|
Please note that we bound ourselves to a :ref:`coc` that applies to all communication around the project. You can be
|
||||||
|
assured that we will not tolerate any form of harassment.
|
||||||
|
|
||||||
Sending a patch
|
Sending a patch
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
Once you have a first draft of your changes, please `create a pull request`_ on our `GitHub repository`_.
|
If you improved pretix in any way, we'd be very happy if you contribute it
|
||||||
|
back to the main code base! The easiest way to do so is to `create a pull request`_
|
||||||
|
on our `GitHub repository`_.
|
||||||
|
|
||||||
We recommend that you create a feature branch for every issue you work on so the changes can
|
We recommend that you create a feature branch for every issue you work on so the changes can
|
||||||
be reviewed individually.
|
be reviewed individually.
|
||||||
@@ -41,17 +25,14 @@ Please use the test suite to check whether your changes break any existing featu
|
|||||||
the code style checks to confirm you are consistent with pretix's coding style. You'll
|
the code style checks to confirm you are consistent with pretix's coding style. You'll
|
||||||
find instructions on this in the :ref:`checksandtests` section of the development setup guide.
|
find instructions on this in the :ref:`checksandtests` section of the development setup guide.
|
||||||
|
|
||||||
We automatically run the tests and the code style check on every pull request through GitHub Actions and we won’t
|
We automatically run the tests and the code style check on every pull request on Travis CI and we won’t
|
||||||
accept any pull requests without all tests passing. However, if you don't find out *why* they are not passing,
|
accept any pull requests without all tests passing. However, if you don't find out *why* they are not passing,
|
||||||
just send the pull request and tell us – we'll be glad to help.
|
just send the pull request and tell us – we'll be glad to help.
|
||||||
|
|
||||||
If you add a new feature, please include appropriate documentation into your patch. If you fix a bug,
|
If you add a new feature, please include appropriate documentation into your patch. If you fix a bug,
|
||||||
please include a regression test, i.e. a test that fails without your changes and passes after applying your changes.
|
please include a regression test, i.e. a test that fails without your changes and passes after applying your changes.
|
||||||
|
|
||||||
Again: If you get stuck, do not hesitate to contact us through GitHub discussions.
|
Again: If you get stuck, do not hesitate to contact any of us, or Raphael personally at mail@raphaelmichel.de.
|
||||||
|
|
||||||
Please note that we bound ourselves to a :ref:`coc` that applies to all communication around the project. You can be
|
|
||||||
assured that we will not tolerate any form of harassment.
|
|
||||||
|
|
||||||
.. _create a pull request: https://help.github.com/articles/creating-a-pull-request/
|
.. _create a pull request: https://help.github.com/articles/creating-a-pull-request/
|
||||||
.. _GitHub repository: https://github.com/pretix/pretix
|
.. _GitHub repository: https://github.com/pretix/pretix
|
||||||
|
|||||||
@@ -6,5 +6,4 @@ Contributing to pretix
|
|||||||
|
|
||||||
general
|
general
|
||||||
style
|
style
|
||||||
ai
|
|
||||||
codeofconduct
|
codeofconduct
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
.. spelling:word-list:: Rebase rebasing
|
.. spelling:word-list:: Rebase rebasing
|
||||||
|
|
||||||
.. _`codestyle`:
|
|
||||||
|
|
||||||
Coding style and quality
|
Coding style and quality
|
||||||
========================
|
========================
|
||||||
|
|
||||||
@@ -30,6 +28,8 @@ Code
|
|||||||
Commits and Pull Requests
|
Commits and Pull Requests
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Most commits should start as pull requests, therefore this applies to the titles of pull requests as well since
|
Most commits should start as pull requests, therefore this applies to the titles of pull requests as well since
|
||||||
the pull request title will become the commit message on merge. We prefer merging with GitHub's "Squash and merge"
|
the pull request title will become the commit message on merge. We prefer merging with GitHub's "Squash and merge"
|
||||||
feature if the PR contains multiple commits that do not carry value to keep. If there is value in keeping the
|
feature if the PR contains multiple commits that do not carry value to keep. If there is value in keeping the
|
||||||
|
|||||||
@@ -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>=46.0.7",
|
"cryptography>=44.0.0",
|
||||||
"css-inline==0.20.*",
|
"css-inline==0.17.*",
|
||||||
"defusedcsv>=3.0.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.58.*",
|
"sentry-sdk==2.35.*",
|
||||||
"sepaxml==2.7.*",
|
"sepaxml==2.6.*",
|
||||||
"stripe==7.9.*",
|
"stripe==7.9.*",
|
||||||
"text-unidecode==1.*",
|
"text-unidecode==1.*",
|
||||||
"tlds>=2026041800",
|
"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,16 +24,14 @@ 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
|
||||||
from pretix.api.serializers.organizer import (
|
from pretix.api.serializers.organizer import (
|
||||||
CustomerSerializer, GiftCardSerializer,
|
CustomerSerializer, GiftCardSerializer,
|
||||||
)
|
)
|
||||||
from pretix.base.models import (
|
from pretix.base.models import Order, OrderPosition, ReusableMedium
|
||||||
Device, Order, OrderPosition, ReusableMedium, TeamAPIToken,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -68,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)
|
||||||
@@ -82,7 +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'):
|
||||||
# Permission Check performed in to_representation
|
|
||||||
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(
|
||||||
@@ -92,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(
|
||||||
@@ -118,27 +109,6 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
|||||||
)
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def to_representation(self, instance):
|
|
||||||
r = super().to_representation(instance)
|
|
||||||
request = self.context.get('request')
|
|
||||||
# late permission evaluations for checks that depend on the actual linked events
|
|
||||||
expand_nested = self.context['request'].query_params.getlist('expand')
|
|
||||||
perm_holder = request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user
|
|
||||||
if 'linked_orderposition' in expand_nested:
|
|
||||||
if instance.linked_orderposition is not None:
|
|
||||||
event = instance.linked_orderposition.order.event
|
|
||||||
if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request):
|
|
||||||
r['linked_orderposition'] = {'id': instance.linked_orderposition.id}
|
|
||||||
|
|
||||||
if 'linked_giftcard.owner_ticket' in expand_nested:
|
|
||||||
gc = instance.linked_giftcard
|
|
||||||
if gc is not None and gc.owner_ticket is not None:
|
|
||||||
event = gc.owner_ticket.order.event
|
|
||||||
if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request):
|
|
||||||
r['linked_giftcard']['owner_ticket'] = {'id': instance.linked_giftcard.owner_ticket.id}
|
|
||||||
|
|
||||||
return r
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ReusableMedium
|
model = ReusableMedium
|
||||||
fields = (
|
fields = (
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -769,11 +730,7 @@ class PaymentDetailsField(serializers.Field):
|
|||||||
pp = value.payment_provider
|
pp = value.payment_provider
|
||||||
if not pp:
|
if not pp:
|
||||||
return {}
|
return {}
|
||||||
try:
|
return pp.api_payment_details(value)
|
||||||
return pp.api_payment_details(value)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to retrieve payment_details")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
class OrderPaymentSerializer(I18nAwareModelSerializer):
|
class OrderPaymentSerializer(I18nAwareModelSerializer):
|
||||||
@@ -876,15 +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):
|
||||||
@@ -1049,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)
|
||||||
@@ -1145,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
|
||||||
|
|
||||||
|
|
||||||
@@ -1203,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()
|
||||||
@@ -1221,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:
|
||||||
@@ -1230,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.')
|
||||||
@@ -1316,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")
|
||||||
@@ -1638,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:
|
||||||
@@ -1767,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()
|
||||||
@@ -1808,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()
|
||||||
@@ -1895,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):
|
||||||
@@ -1916,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
|
||||||
|
|
||||||
@@ -286,19 +279,6 @@ class GiftCardSerializer(I18nAwareModelSerializer):
|
|||||||
)
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def to_representation(self, instance):
|
|
||||||
r = super().to_representation(instance)
|
|
||||||
request = self.context.get('request')
|
|
||||||
# late permission evaluations for checks that depend on the actual linked events
|
|
||||||
if 'owner_ticket' in self.context['request'].query_params.getlist('expand'):
|
|
||||||
owner_ticket = instance.owner_ticket
|
|
||||||
if owner_ticket:
|
|
||||||
event = owner_ticket.order.event
|
|
||||||
perm_holder = request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user
|
|
||||||
if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request):
|
|
||||||
r['owner_ticket'] = {'id': instance.owner_ticket.id}
|
|
||||||
return r
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = GiftCard
|
model = GiftCard
|
||||||
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions', 'owner_ticket',
|
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions', 'owner_ticket',
|
||||||
@@ -326,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
|
||||||
|
|
||||||
|
|
||||||
@@ -464,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:
|
||||||
@@ -478,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):
|
||||||
@@ -490,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:
|
||||||
@@ -564,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',
|
||||||
@@ -613,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(list__event=self.request.event).select_related(
|
|
||||||
"position",
|
|
||||||
"device",
|
|
||||||
)
|
|
||||||
return qs
|
|
||||||
|
|
||||||
def get_serializer_context(self):
|
|
||||||
ctx = super().get_serializer_context()
|
|
||||||
ctx['event'] = self.request.event
|
|
||||||
return ctx
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -57,7 +57,7 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
ordering_fields = ('id', 'position')
|
ordering_fields = ('id', 'position')
|
||||||
ordering = ('position', 'id')
|
ordering = ('position', 'id')
|
||||||
permission = None
|
permission = None
|
||||||
write_permission = 'event.items:write'
|
write_permission = 'can_change_items'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.discounts.prefetch_related(
|
return self.request.event.discounts.prefetch_related(
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#
|
#
|
||||||
# This file is part of pretix (Community Edition).
|
# This file is part of pretix (Community Edition).
|
||||||
#
|
#
|
||||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
@@ -281,11 +281,6 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
new_event = serializer.save(organizer=self.request.organizer)
|
new_event = serializer.save(organizer=self.request.organizer)
|
||||||
|
|
||||||
if copy_from:
|
if copy_from:
|
||||||
perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken))
|
|
||||||
else self.request.user)
|
|
||||||
if not copy_from.allow_copy_data(self.request.organizer, perm_holder):
|
|
||||||
raise PermissionDenied("Not sufficient permission on source event to copy")
|
|
||||||
|
|
||||||
new_event.copy_data_from(copy_from, skip_meta_data='meta_data' in serializer.validated_data)
|
new_event.copy_data_from(copy_from, skip_meta_data='meta_data' in serializer.validated_data)
|
||||||
|
|
||||||
if plugins is not None:
|
if plugins is not None:
|
||||||
@@ -346,24 +341,15 @@ class CloneEventViewSet(viewsets.ModelViewSet):
|
|||||||
lookup_field = 'slug'
|
lookup_field = 'slug'
|
||||||
lookup_url_kwarg = 'event'
|
lookup_url_kwarg = 'event'
|
||||||
http_method_names = ['post']
|
http_method_names = ['post']
|
||||||
write_permission = 'event.settings.general:write'
|
write_permission = 'can_create_events'
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
ctx['event'] = Event.objects.get(slug=self.kwargs['event'], organizer=self.request.organizer)
|
ctx['event'] = self.kwargs['event']
|
||||||
ctx['organizer'] = self.request.organizer
|
ctx['organizer'] = self.request.organizer
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
# Weird edge case: Requires settings permission on the event (to read) but also on the organizer (two write)
|
|
||||||
perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken))
|
|
||||||
else self.request.user)
|
|
||||||
if not perm_holder.has_organizer_permission(self.request.organizer, "organizer.events:create", request=self.request):
|
|
||||||
raise PermissionDenied("No permission to create events")
|
|
||||||
|
|
||||||
if not serializer.context['event'].allow_copy_data(self.request.organizer, perm_holder):
|
|
||||||
raise PermissionDenied("Not sufficient permission on source event to copy")
|
|
||||||
|
|
||||||
serializer.save(organizer=self.request.organizer)
|
serializer.save(organizer=self.request.organizer)
|
||||||
|
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
@@ -440,7 +426,7 @@ with scopes_disabled():
|
|||||||
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = SubEventSerializer
|
serializer_class = SubEventSerializer
|
||||||
queryset = SubEvent.objects.none()
|
queryset = SubEvent.objects.none()
|
||||||
write_permission = 'event.subevents:write'
|
write_permission = 'can_change_event_settings'
|
||||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
||||||
ordering = ('date_from',)
|
ordering = ('date_from',)
|
||||||
ordering_fields = ('id', 'date_from', 'last_modified')
|
ordering_fields = ('id', 'date_from', 'last_modified')
|
||||||
@@ -560,7 +546,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
|||||||
class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||||
serializer_class = TaxRuleSerializer
|
serializer_class = TaxRuleSerializer
|
||||||
queryset = TaxRule.objects.none()
|
queryset = TaxRule.objects.none()
|
||||||
write_permission = 'event.settings.tax:write'
|
write_permission = 'can_change_event_settings'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.tax_rules.all()
|
return self.request.event.tax_rules.all()
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -381,15 +380,12 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
|||||||
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
|
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
|
||||||
return resp
|
return resp
|
||||||
else:
|
else:
|
||||||
return FileResponse(
|
resp = FileResponse(ct.file.file, content_type=ct.type)
|
||||||
ct.file.file,
|
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
|
||||||
filename='{}-{}-{}{}'.format(
|
self.request.event.slug.upper(), order.code,
|
||||||
self.request.event.slug.upper(), order.code,
|
provider.identifier, ct.extension
|
||||||
provider.identifier, ct.extension
|
|
||||||
),
|
|
||||||
as_attachment=True,
|
|
||||||
content_type=ct.type
|
|
||||||
)
|
)
|
||||||
|
return resp
|
||||||
|
|
||||||
@action(detail=True, methods=['POST'])
|
@action(detail=True, methods=['POST'])
|
||||||
def mark_paid(self, request, **kwargs):
|
def mark_paid(self, request, **kwargs):
|
||||||
@@ -442,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(
|
||||||
@@ -635,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
|
||||||
@@ -742,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(
|
||||||
@@ -763,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))
|
||||||
@@ -1069,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),
|
||||||
@@ -1088,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
|
||||||
|
|
||||||
@@ -1097,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],
|
||||||
@@ -1153,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
|
||||||
|
|
||||||
@@ -1167,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):
|
||||||
"""
|
"""
|
||||||
@@ -1306,17 +1263,14 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
|
|||||||
raise NotFound()
|
raise NotFound()
|
||||||
|
|
||||||
ftype, ignored = mimetypes.guess_type(answer.file.name)
|
ftype, ignored = mimetypes.guess_type(answer.file.name)
|
||||||
return FileResponse(
|
resp = FileResponse(answer.file, content_type=ftype or 'application/binary')
|
||||||
answer.file,
|
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}"'.format(
|
||||||
filename='{}-{}-{}-{}'.format(
|
self.request.event.slug.upper(),
|
||||||
self.request.event.slug.upper(),
|
pos.order.code,
|
||||||
pos.order.code,
|
pos.positionid,
|
||||||
pos.positionid,
|
os.path.basename(answer.file.name).split('.', 1)[1]
|
||||||
os.path.basename(answer.file.name).split('.', 1)[1]
|
|
||||||
),
|
|
||||||
as_attachment=True,
|
|
||||||
content_type=ftype or 'application/binary'
|
|
||||||
)
|
)
|
||||||
|
return resp
|
||||||
|
|
||||||
@action(detail=True, url_name="printlog", url_path="printlog", methods=["POST"])
|
@action(detail=True, url_name="printlog", url_path="printlog", methods=["POST"])
|
||||||
def printlog(self, request, **kwargs):
|
def printlog(self, request, **kwargs):
|
||||||
@@ -1371,18 +1325,15 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
|
|||||||
if hasattr(image_file, 'seek'):
|
if hasattr(image_file, 'seek'):
|
||||||
image_file.seek(0)
|
image_file.seek(0)
|
||||||
|
|
||||||
return FileResponse(
|
resp = FileResponse(image_file, content_type=ftype or 'application/binary')
|
||||||
image_file,
|
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}.{}"'.format(
|
||||||
filename='{}-{}-{}-{}.{}'.format(
|
self.request.event.slug.upper(),
|
||||||
self.request.event.slug.upper(),
|
pos.order.code,
|
||||||
pos.order.code,
|
pos.positionid,
|
||||||
pos.positionid,
|
key,
|
||||||
key,
|
extension,
|
||||||
extension,
|
|
||||||
),
|
|
||||||
as_attachment=True,
|
|
||||||
content_type=ftype or 'application/binary'
|
|
||||||
)
|
)
|
||||||
|
return resp
|
||||||
|
|
||||||
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
|
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
|
||||||
def download(self, request, output, **kwargs):
|
def download(self, request, output, **kwargs):
|
||||||
@@ -1408,15 +1359,12 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
|
|||||||
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
|
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
|
||||||
return resp
|
return resp
|
||||||
else:
|
else:
|
||||||
return FileResponse(
|
resp = FileResponse(ct.file.file, content_type=ct.type)
|
||||||
ct.file.file,
|
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
|
||||||
filename='{}-{}-{}-{}{}'.format(
|
self.request.event.slug.upper(), pos.order.code, pos.positionid,
|
||||||
self.request.event.slug.upper(), pos.order.code, pos.positionid,
|
provider.identifier, ct.extension
|
||||||
provider.identifier, ct.extension
|
|
||||||
),
|
|
||||||
as_attachment=True,
|
|
||||||
content_type=ct.type
|
|
||||||
)
|
)
|
||||||
|
return resp
|
||||||
|
|
||||||
@action(detail=True, methods=['POST'])
|
@action(detail=True, methods=['POST'])
|
||||||
def regenerate_secrets(self, request, **kwargs):
|
def regenerate_secrets(self, request, **kwargs):
|
||||||
@@ -1625,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):
|
||||||
@@ -1661,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)
|
||||||
|
|
||||||
@@ -1698,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'])
|
||||||
@@ -1711,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)
|
||||||
|
|
||||||
@@ -1740,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,
|
||||||
@@ -1798,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):
|
||||||
@@ -1956,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)):
|
||||||
@@ -1998,12 +1941,9 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
if not invoice.file:
|
if not invoice.file:
|
||||||
raise RetryException()
|
raise RetryException()
|
||||||
|
|
||||||
return FileResponse(
|
resp = FileResponse(invoice.file.file, content_type='application/pdf')
|
||||||
invoice.file.file,
|
resp['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(invoice.number)
|
||||||
filename='{}.pdf'.format(invoice.number),
|
return resp
|
||||||
as_attachment=True,
|
|
||||||
content_type='application/pdf'
|
|
||||||
)
|
|
||||||
|
|
||||||
@action(detail=True, methods=['POST'])
|
@action(detail=True, methods=['POST'])
|
||||||
def transmit(self, request, **kwargs):
|
def transmit(self, request, **kwargs):
|
||||||
@@ -2080,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
|
||||||
@@ -2111,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)
|
||||||
@@ -2133,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)
|
||||||
@@ -2169,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")
|
||||||
@@ -2186,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.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user