mirror of
https://github.com/pretix/pretix.git
synced 2026-01-13 22:52:27 +00:00
Compare commits
83 Commits
recovery-c
...
pluggable-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a5d182d32 | ||
|
|
b064af8d43 | ||
|
|
8cc622bee2 | ||
|
|
918694ca3d | ||
|
|
1fbc062a45 | ||
|
|
acba13dc68 | ||
|
|
a973484505 | ||
|
|
91d6273f13 | ||
|
|
65fe1f9cdb | ||
|
|
560a4019b2 | ||
|
|
47d0505306 | ||
|
|
871bdebbe6 | ||
|
|
09b66d28c8 | ||
|
|
5d56daeb64 | ||
|
|
41749cc942 | ||
|
|
62bc16f963 | ||
|
|
3332fc818a | ||
|
|
d87dbaf9e5 | ||
|
|
67580c4ca5 | ||
|
|
c5b32484b1 | ||
|
|
b5560509ad | ||
|
|
c78365ce43 | ||
|
|
8cc12fa1c7 | ||
|
|
59c09e27fd | ||
|
|
4d68d24eca | ||
|
|
cc5693017e | ||
|
|
6a07b7d5d1 | ||
|
|
26dc3486a0 | ||
|
|
de60183456 | ||
|
|
520bb9e378 | ||
|
|
97e344e81a | ||
|
|
a3f5f33ed5 | ||
|
|
5a123bf88f | ||
|
|
64c52a5e36 | ||
|
|
a60341afe9 | ||
|
|
308e14bab3 | ||
|
|
aa5f635932 | ||
|
|
66a9902eb4 | ||
|
|
79a58fe104 | ||
|
|
bb5a9bdbf1 | ||
|
|
449b960438 | ||
|
|
a3f247117c | ||
|
|
e279ecb423 | ||
|
|
ca6a650398 | ||
|
|
696e5602ac | ||
|
|
4c7987cef6 | ||
|
|
37c65030f8 | ||
|
|
0d1673136f | ||
|
|
32d8dce6aa | ||
|
|
8a2ecb4e97 | ||
|
|
91348e3b00 | ||
|
|
459f4f84c7 | ||
|
|
31a1385946 | ||
|
|
adfd0bfcfd | ||
|
|
ef7433dbcd | ||
|
|
ebbd18bb26 | ||
|
|
fc4ce102b6 | ||
|
|
8854ae3187 | ||
|
|
c5a91ef479 | ||
|
|
aa9c478c30 | ||
|
|
847dc0f992 | ||
|
|
daaae85865 | ||
|
|
06770bcef5 | ||
|
|
dc6eae4708 | ||
|
|
bf8bb78d2a | ||
|
|
091be266fc | ||
|
|
dde655f7d6 | ||
|
|
409e64d5f2 | ||
|
|
5d67a4fa33 | ||
|
|
4eb2c50d95 | ||
|
|
a7e85a157d | ||
|
|
4c3584c788 | ||
|
|
e466c4fb72 | ||
|
|
d0d7670ca5 | ||
|
|
a17a098b15 | ||
|
|
40516ab8e0 | ||
|
|
3ca343fabc | ||
|
|
7304b7f24b | ||
|
|
abaf968103 | ||
|
|
86e2f5a155 | ||
|
|
4c64af02c1 | ||
|
|
11df4398e1 | ||
|
|
2e89fc0a94 |
4
.github/workflows/docs.yml
vendored
4
.github/workflows/docs.yml
vendored
@@ -26,10 +26,10 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.11
|
||||
- name: Set up Python 3.13
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
python-version: 3.13
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
|
||||
174
doc/_themes/pretix_theme/layout.html
vendored
174
doc/_themes/pretix_theme/layout.html
vendored
@@ -6,10 +6,14 @@
|
||||
{%- else %}
|
||||
{%- set titlesuffix = "" %}
|
||||
{%- 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>
|
||||
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
|
||||
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
|
||||
<html class="writer-html5" lang="{{ lang_attr }}"{% if sphinx_version_info >= (7, 2) %} data-content_root="{{ content_root }}"{% endif %}>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
{{ metatags }}
|
||||
@@ -18,59 +22,50 @@
|
||||
<title>{{ title|striptags|e }}{{ titlesuffix }}</title>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{#- CSS #}
|
||||
{%- for css in css_files %}
|
||||
{%- 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 %} />
|
||||
{#- CSS #}
|
||||
{%- for css_file in css_files %}
|
||||
{%- if css_file|attr("filename") %}
|
||||
{{ css_tag(css_file) }}
|
||||
{%- else %}
|
||||
<link rel="stylesheet" href="{{ pathto(css, 1) }}" type="text/css" />
|
||||
<link rel="stylesheet" href="{{ pathto(css_file, 1)|escape }}" type="text/css" />
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{%- endfor %}
|
||||
|
||||
{%- for cssfile in extra_css_files %}
|
||||
<link rel="stylesheet" href="{{ pathto(cssfile, 1) }}" type="text/css" />
|
||||
{%- endfor -%}
|
||||
{#- FAVICON #}
|
||||
{%- if favicon_url %}
|
||||
<link rel="shortcut icon" href="{{ favicon_url }}"/>
|
||||
{%- endif %}
|
||||
|
||||
{#- FAVICON
|
||||
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 %}
|
||||
{#- CANONICAL URL (deprecated) #}
|
||||
{%- if theme_canonical_url and not pageurl %}
|
||||
<link rel="canonical" href="{{ theme_canonical_url }}{{ pagename }}.html"/>
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
|
||||
{#- CANONICAL URL #}
|
||||
{%- if pageurl %}
|
||||
{#- CANONICAL URL #}
|
||||
{%- if pageurl %}
|
||||
<link rel="canonical" href="{{ pageurl|e }}" />
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
|
||||
{#- JAVASCRIPTS #}
|
||||
{%- block scripts %}
|
||||
<!--[if lt IE 9]>
|
||||
<script src="{{ pathto('_static/js/html5shiv.min.js', 1) }}"></script>
|
||||
<![endif]-->
|
||||
{%- 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 %}
|
||||
{#- JAVASCRIPTS #}
|
||||
{%- block scripts %}
|
||||
{%- if not embedded %}
|
||||
{%- for scriptfile in script_files %}
|
||||
{{ js_tag(scriptfile) }}
|
||||
{%- endfor %}
|
||||
<script src="{{ pathto('_static/js/theme.js', 1) }}"></script>
|
||||
|
||||
{%- if READTHEDOCS or DEBUG %}
|
||||
<script src="{{ pathto('_static/js/versions.js', 1) }}"></script>
|
||||
{%- endif %}
|
||||
|
||||
{#- OPENSEARCH #}
|
||||
{%- if use_opensearch %}
|
||||
<link rel="search" type="application/opensearchdescription+xml"
|
||||
title="{% trans docstitle=docstitle|e %}Search within {{ docstitle }}{% endtrans %}"
|
||||
href="{{ pathto('_static/opensearch.xml', 1) }}"/>
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
{%- endblock %}
|
||||
{%- endif %}
|
||||
{%- endblock %}
|
||||
|
||||
{%- block linktags %}
|
||||
{%- if hasdoc('about') %}
|
||||
@@ -123,23 +118,23 @@
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
|
||||
{% block menu %}
|
||||
{#
|
||||
The singlehtml builder doesn't handle this toctree call when the
|
||||
toctree is empty. Skip building this for now.
|
||||
#}
|
||||
{% if 'singlehtml' not in builder %}
|
||||
{% set global_toc = toctree(maxdepth=theme_navigation_depth|int, collapse=theme_collapse_navigation, includehidden=True) %}
|
||||
{% endif %}
|
||||
{% if global_toc %}
|
||||
{{ global_toc }}
|
||||
{% else %}
|
||||
{%- 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>
|
||||
{%- endif %}
|
||||
{%- endblock %}
|
||||
</div>
|
||||
{%- endblock %}
|
||||
|
||||
{% if theme_display_version %}
|
||||
{%- set nav_version = version %}
|
||||
@@ -158,53 +153,42 @@
|
||||
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
|
||||
|
||||
{# MOBILE NAV, TRIGGLES SIDE NAV ON TOGGLE #}
|
||||
<nav class="wy-nav-top" role="navigation" aria-label="top navigation">
|
||||
{% block mobile_nav %}
|
||||
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
|
||||
<a href="{{ pathto('index') }}">{{ project }}</a>
|
||||
{% endblock %}
|
||||
</nav>
|
||||
<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>
|
||||
|
||||
|
||||
{# PAGE CONTENT #}
|
||||
<div class="wy-nav-content">
|
||||
<div class="rst-content">
|
||||
{% include "breadcrumbs.html" %}
|
||||
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
|
||||
<div itemprop="articleBody" class="section">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
<div class="articleComments">
|
||||
{% block comments %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "footer.html" %}
|
||||
<div class="wy-nav-content">
|
||||
{%- block content %}
|
||||
{%- if theme_style_external_links|tobool %}
|
||||
<div class="rst-content style-external-links">
|
||||
{%- else %}
|
||||
<div class="rst-content">
|
||||
{%- endif %}
|
||||
{% include "breadcrumbs.html" %}
|
||||
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
|
||||
{%- block document %}
|
||||
<div itemprop="articleBody">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
{%- if self.comments()|trim %}
|
||||
<div class="articleComments">
|
||||
{%- block comments %}{% endblock %}
|
||||
</div>
|
||||
{%- endif%}
|
||||
</div>
|
||||
{%- endblock %}
|
||||
{% include "footer.html" %}
|
||||
</div>
|
||||
{%- endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</div>
|
||||
{% 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 #}
|
||||
{% if not READTHEDOCS %}
|
||||
<script type="text/javascript" src="{{ pathto('_static/js/theme.js', 1) }}"></script>
|
||||
@@ -214,7 +198,7 @@
|
||||
{% if theme_sticky_navigation %}
|
||||
<script type="text/javascript">
|
||||
jQuery(function () {
|
||||
SphinxRtdTheme.StickyNav.enable();
|
||||
SphinxRtdTheme.Navigation.enable({{ 'true' if theme_sticky_navigation|tobool else 'false' }});
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
350
doc/_themes/pretix_theme/layout_old.html
vendored
350
doc/_themes/pretix_theme/layout_old.html
vendored
@@ -1,136 +1,86 @@
|
||||
{#
|
||||
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 != []) %}
|
||||
{# TEMPLATE VAR SETTINGS #}
|
||||
{%- set url_root = pathto('', 1) %}
|
||||
{# XXX necessary? #}
|
||||
{%- if url_root == '#' %}{% set url_root = '' %}{% endif %}
|
||||
{%- if not embedded and docstitle %}
|
||||
{%- set titlesuffix = " — "|safe + docstitle|e %}
|
||||
{%- else %}
|
||||
{%- set titlesuffix = "" %}
|
||||
{%- endif %}
|
||||
{%- set lang_attr = 'en' if language == None else (language | replace('_', '-')) %}
|
||||
|
||||
{%- macro relbar() %}
|
||||
<div class="related">
|
||||
<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 %}
|
||||
{# 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) -%}
|
||||
|
||||
{%- macro sidebar() %}
|
||||
{%- if render_sidebar %}
|
||||
<div class="sphinxsidebar">
|
||||
<div class="sphinxsidebarwrapper">
|
||||
{%- block sidebarlogo %}
|
||||
{%- if logo %}
|
||||
<p class="logo"><a href="{{ pathto(master_doc) }}">
|
||||
<img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/>
|
||||
</a></p>
|
||||
{%- endif %}
|
||||
{%- 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 %}
|
||||
<!DOCTYPE html>
|
||||
<html class="writer-html5" lang="{{ lang_attr }}"{% if sphinx_version_info >= (7, 2) %} data-content_root="{{ content_root }}"{% endif %}>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
{%- if READTHEDOCS and not embedded %}
|
||||
<meta name="readthedocs-addons-api-version" content="1">
|
||||
{%- endif %}
|
||||
{{- metatags }}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
{%- block htmltitle %}
|
||||
<title>{{ title|striptags|e }}{{ titlesuffix }}</title>
|
||||
{%- endblock -%}
|
||||
|
||||
{%- macro script() %}
|
||||
<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>
|
||||
{#- CSS #}
|
||||
{%- for css_file in css_files %}
|
||||
{%- if css_file|attr("filename") %}
|
||||
{{ css_tag(css_file) }}
|
||||
{%- else %}
|
||||
<link rel="stylesheet" href="{{ pathto(css_file, 1)|escape }}" type="text/css" />
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
|
||||
{#
|
||||
"extra_css_files" is an undocumented Read the Docs theme specific option.
|
||||
There is no need to check for ``|attr("filename")`` here because it's always a string.
|
||||
Note that this option should be removed in favor of regular ``html_css_files``:
|
||||
https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_css_files
|
||||
#}
|
||||
{%- for css_file in extra_css_files %}
|
||||
<link rel="stylesheet" href="{{ pathto(css_file, 1)|escape }}" type="text/css" />
|
||||
{%- endfor -%}
|
||||
|
||||
{#- FAVICON #}
|
||||
{%- if favicon_url %}
|
||||
<link rel="shortcut icon" href="{{ favicon_url }}"/>
|
||||
{%- endif %}
|
||||
|
||||
{#- CANONICAL URL (deprecated) #}
|
||||
{%- if theme_canonical_url and not pageurl %}
|
||||
<link rel="canonical" href="{{ theme_canonical_url }}{{ pagename }}.html"/>
|
||||
{%- endif -%}
|
||||
|
||||
{#- CANONICAL URL #}
|
||||
{%- if pageurl %}
|
||||
<link rel="canonical" href="{{ pageurl|e }}" />
|
||||
{%- endif -%}
|
||||
|
||||
{#- JAVASCRIPTS #}
|
||||
{%- block scripts %}
|
||||
{%- if not embedded %}
|
||||
{%- for scriptfile in script_files %}
|
||||
<script type="text/javascript" src="{{ pathto(scriptfile, 1) }}"></script>
|
||||
{{ js_tag(scriptfile) }}
|
||||
{%- endfor %}
|
||||
{%- endmacro %}
|
||||
<script src="{{ pathto('_static/js/theme.js', 1) }}"></script>
|
||||
|
||||
{%- macro css() %}
|
||||
<link rel="stylesheet" href="{{ pathto('_static/' + style, 1) }}" type="text/css" />
|
||||
<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 %}
|
||||
{%- if READTHEDOCS or DEBUG %}
|
||||
<script src="{{ pathto('_static/js/versions.js', 1) }}"></script>
|
||||
{%- endif %}
|
||||
|
||||
<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() }}
|
||||
{#- OPENSEARCH #}
|
||||
{%- if use_opensearch %}
|
||||
<link rel="search" type="application/opensearchdescription+xml"
|
||||
title="{% trans docstitle=docstitle|e %}Search within {{ docstitle }}{% endtrans %}"
|
||||
href="{{ pathto('_static/opensearch.xml', 1) }}"/>
|
||||
{%- endif %}
|
||||
{%- if favicon %}
|
||||
<link rel="shortcut icon" href="{{ pathto('_static/' + favicon, 1) }}"/>
|
||||
{%- endif %}
|
||||
{%- if theme_canonical_url %}
|
||||
<link rel="canonical" href="{{ theme_canonical_url }}{{ pagename }}.html"/>
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
{%- block linktags %}
|
||||
{%- endif %}
|
||||
{%- endblock %}
|
||||
|
||||
{%- block linktags %}
|
||||
{%- if hasdoc('about') %}
|
||||
<link rel="author" title="{{ _('About these documents') }}" href="{{ pathto('about') }}" />
|
||||
{%- endif %}
|
||||
@@ -143,67 +93,135 @@
|
||||
{%- if hasdoc('copyright') %}
|
||||
<link rel="copyright" title="{{ _('Copyright') }}" href="{{ pathto('copyright') }}" />
|
||||
{%- 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 %}
|
||||
<link rel="next" title="{{ next.title|striptags|e }}" href="{{ next.link|e }}" />
|
||||
{%- endif %}
|
||||
{%- if prev %}
|
||||
<link rel="prev" title="{{ prev.title|striptags|e }}" href="{{ prev.link|e }}" />
|
||||
{%- endif %}
|
||||
{%- endblock %}
|
||||
{%- block extrahead %} {% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{%- block header %}{% endblock %}
|
||||
|
||||
{%- block relbar1 %}{{ relbar() }}{% endblock %}
|
||||
|
||||
{%- block content %}
|
||||
{%- block sidebar1 %} {# possible location for sidebar #} {% endblock %}
|
||||
|
||||
<div class="document">
|
||||
{%- block document %}
|
||||
<div class="documentwrapper">
|
||||
{%- if render_sidebar %}
|
||||
<div class="bodywrapper">
|
||||
{%- endif %}
|
||||
<div class="body">
|
||||
{% block body %} {% endblock %}
|
||||
</div>
|
||||
{%- if render_sidebar %}
|
||||
</div>
|
||||
{%- endif %}
|
||||
</div>
|
||||
{%- endblock %}
|
||||
{%- block extrahead %} {% endblock %}
|
||||
</head>
|
||||
|
||||
{%- block sidebar2 %}{{ sidebar() }}{% endblock %}
|
||||
<div class="clearer"></div>
|
||||
</div>
|
||||
{%- endblock %}
|
||||
<body class="wy-body-for-nav">
|
||||
|
||||
{%- block relbar2 %}{{ relbar() }}{% endblock %}
|
||||
{%- block extrabody %} {% endblock %}
|
||||
<div class="wy-grid-for-nav">
|
||||
{#- SIDE NAV, TOGGLES ON MOBILE #}
|
||||
<nav data-toggle="wy-nav-shift" class="wy-nav-side">
|
||||
<div class="wy-side-scroll">
|
||||
<div class="wy-side-nav-search" {% if theme_style_nav_header_background %} style="background: {{theme_style_nav_header_background}}" {% endif %}>
|
||||
{%- block sidebartitle %}
|
||||
|
||||
{# the logo helper function was removed in Sphinx 6 and deprecated since Sphinx 4 #}
|
||||
{# the master_doc variable was renamed to root_doc in Sphinx 4 (master_doc still exists in later Sphinx versions) #}
|
||||
{%- set _logo_url = logo_url|default(pathto('_static/' + (logo or ""), 1)) %}
|
||||
{%- set _root_doc = root_doc|default(master_doc) %}
|
||||
<a href="{{ pathto(_root_doc) }}"{% if not theme_logo_only %} class="icon icon-home"{% endif %}>
|
||||
{% if not theme_logo_only %}{{ project }}{% endif %}
|
||||
{%- if logo or logo_url %}
|
||||
<img src="{{ _logo_url }}" class="logo" alt="{{ _('Logo') }}"/>
|
||||
{%- endif %}
|
||||
</a>
|
||||
|
||||
{%- if READTHEDOCS or DEBUG %}
|
||||
{%- if theme_version_selector or theme_language_selector %}
|
||||
<div class="switch-menus">
|
||||
<div class="version-switch"></div>
|
||||
<div class="language-switch"></div>
|
||||
</div>
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
|
||||
{%- include "searchbox.html" %}
|
||||
|
||||
{%- endblock %}
|
||||
</div>
|
||||
|
||||
{%- block navigation %}
|
||||
{#- Translators: This is an ARIA section label for the main navigation menu -#}
|
||||
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="{{ _('Navigation menu') }}">
|
||||
{%- block menu %}
|
||||
{%- set toctree = toctree(maxdepth=theme_navigation_depth|int,
|
||||
collapse=theme_collapse_navigation|tobool,
|
||||
includehidden=theme_includehidden|tobool,
|
||||
titles_only=theme_titles_only|tobool) %}
|
||||
{%- if toctree %}
|
||||
{{ toctree }}
|
||||
{%- else %}
|
||||
<!-- Local TOC -->
|
||||
<div class="local-toc">{{ toc }}</div>
|
||||
{%- endif %}
|
||||
{%- endblock %}
|
||||
</div>
|
||||
{%- endblock %}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
|
||||
|
||||
{#- MOBILE NAV, TRIGGLES SIDE NAV ON TOGGLE #}
|
||||
{#- Translators: This is an ARIA section label for the navigation menu that is visible when viewing the page on mobile devices -#}
|
||||
<nav class="wy-nav-top" aria-label="{{ _('Mobile navigation menu') }}" {% if theme_style_nav_header_background %} style="background: {{theme_style_nav_header_background}}" {% endif %}>
|
||||
{%- block mobile_nav %}
|
||||
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
|
||||
<a href="{{ pathto(master_doc) }}">{{ project }}</a>
|
||||
{%- endblock %}
|
||||
</nav>
|
||||
|
||||
<div class="wy-nav-content">
|
||||
{%- block content %}
|
||||
{%- if theme_style_external_links|tobool %}
|
||||
<div class="rst-content style-external-links">
|
||||
{%- else %}
|
||||
<div class="rst-content">
|
||||
{%- endif %}
|
||||
{% include "breadcrumbs.html" %}
|
||||
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
|
||||
{%- block document %}
|
||||
<div itemprop="articleBody">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
{%- if self.comments()|trim %}
|
||||
<div class="articleComments">
|
||||
{%- block comments %}{% endblock %}
|
||||
</div>
|
||||
{%- endif%}
|
||||
</div>
|
||||
{%- endblock %}
|
||||
{% include "footer.html" %}
|
||||
</div>
|
||||
{%- endblock %}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% include "versions.html" -%}
|
||||
|
||||
<script>
|
||||
jQuery(function () {
|
||||
SphinxRtdTheme.Navigation.enable({{ 'true' if theme_sticky_navigation|tobool else 'false' }});
|
||||
});
|
||||
</script>
|
||||
|
||||
{#- Do not conflict with RTD insertion of analytics script #}
|
||||
{%- if not READTHEDOCS %}
|
||||
{%- if theme_analytics_id %}
|
||||
<!-- Theme Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id={{ theme_analytics_id }}"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', '{{ theme_analytics_id }}', {
|
||||
'anonymize_ip': {{ 'true' if theme_analytics_anonymize_ip|tobool else 'false' }},
|
||||
});
|
||||
</script>
|
||||
|
||||
{%- block footer %}
|
||||
<div class="footer">
|
||||
{%- if show_copyright %}
|
||||
{%- if hasdoc('copyright') %}
|
||||
{% trans path=pathto('copyright'), copyright=copyright|e %}© <a href="{{ path }}">Copyright</a> {{ copyright }}.{% endtrans %}
|
||||
{%- else %}
|
||||
{% trans copyright=copyright|e %}© Copyright {{ copyright }}.{% endtrans %}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
{%- 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>
|
||||
{%- endif %}
|
||||
|
||||
{%- 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"
|
||||
}
|
||||
|
||||
The ``rsa_pubkey`` is optional any only required for certain fatures such as working with reusable
|
||||
The ``rsa_pubkey`` is optional any only required for certain features such as working with reusable
|
||||
media and NFC cryptography.
|
||||
|
||||
Every initialization token can only be used once. On success, you will receive a response containing
|
||||
@@ -197,10 +197,11 @@ Permissions & security profiles
|
||||
|
||||
Device authentication is currently hardcoded to grant the following permissions:
|
||||
|
||||
* View event meta data and products etc.
|
||||
* View orders
|
||||
* Change orders
|
||||
* Manage gift cards
|
||||
* Read event meta data and products etc.
|
||||
* Read and write orders
|
||||
* Read and write gift cards
|
||||
* Read and write reusable media
|
||||
* Read vouchers
|
||||
|
||||
Devices cannot change events or products and cannot access vouchers.
|
||||
|
||||
|
||||
@@ -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.
|
||||
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
|
||||
``If-Modified-Since`` header. If the any object has changed in the meantime, you will receive back a full list
|
||||
``If-Modified-Since`` header. If any object has changed in the meantime, you will receive back a full list
|
||||
(if something it missing, this means the object has been deleted). If nothing happened, we'll send back a
|
||||
``304 Not Modified`` return code.
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ software_brand string Device software
|
||||
software_version string Device software version (read-only)
|
||||
created datetime Creation time
|
||||
initialized datetime Time of initialization (or ``null``)
|
||||
initialization_token string Token for initialization
|
||||
initialization_token string Token for initialization (field invisible without without write permission)
|
||||
revoked boolean Whether this device no longer has access
|
||||
security_profile string The name of a supported security profile restricting API access
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
@@ -65,8 +65,6 @@ Endpoints
|
||||
|
||||
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**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -161,8 +159,6 @@ Endpoints
|
||||
|
||||
Returns information on one event, identified by its slug.
|
||||
|
||||
Permission required: "Can change event settings"
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -234,8 +230,6 @@ Endpoints
|
||||
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.
|
||||
|
||||
Permission required: "Can create events"
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -338,8 +332,6 @@ Endpoints
|
||||
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.
|
||||
|
||||
Permission required: "Can create events"
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -433,8 +425,6 @@ Endpoints
|
||||
|
||||
Updates an event
|
||||
|
||||
Permission required: "Can change event settings"
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -510,8 +500,6 @@ Endpoints
|
||||
|
||||
Delete an event. Note that events with orders cannot be deleted to ensure data integrity.
|
||||
|
||||
Permission required: "Can change event settings"
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -561,8 +549,6 @@ organizer level.
|
||||
|
||||
Get current values of event settings.
|
||||
|
||||
Permission required: "Can change event settings" (Exception: with device auth, *some* settings can always be *read*.)
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -615,6 +601,8 @@ organizer level.
|
||||
|
||||
Updates event settings. Note that ``PUT`` is not allowed here, only ``PATCH``.
|
||||
|
||||
Permission "Can change event settings" is always required. Some keys requrie additional permissions.
|
||||
|
||||
.. warning::
|
||||
|
||||
Settings can be stored at different levels in pretix. If a value is not set on event level, a default setting
|
||||
|
||||
@@ -46,28 +46,28 @@ Endpoints
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
{
|
||||
"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
|
||||
|
||||
@@ -110,8 +110,6 @@ Endpoints
|
||||
|
||||
Updates an organizer. Currently only the ``plugins`` field may be updated.
|
||||
|
||||
Permission required: "Can change organizer settings"
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -172,8 +170,6 @@ information about the properties.
|
||||
|
||||
Get current values of organizer settings.
|
||||
|
||||
Permission required: "Can change organizer settings"
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
@@ -154,7 +154,7 @@ Endpoints
|
||||
.. 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
|
||||
medium behind the scenes.
|
||||
medium behind the scenes, therefore this endpoint requires write permissions.
|
||||
|
||||
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,
|
||||
|
||||
@@ -154,8 +154,6 @@ Endpoints
|
||||
|
||||
Creates a new subevent.
|
||||
|
||||
Permission required: "Can create events"
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -300,8 +298,6 @@ Endpoints
|
||||
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.
|
||||
|
||||
Permission required: "Can change event settings"
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@@ -373,8 +369,6 @@ Endpoints
|
||||
|
||||
Delete a sub-event. Note that events with orders cannot be deleted to ensure data integrity.
|
||||
|
||||
Permission required: "Can change event settings"
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
@@ -24,21 +24,57 @@ all_events boolean Whether this te
|
||||
limit_events list List of event slugs this team has access to
|
||||
require_2fa boolean Whether members of this team are required to use
|
||||
two-factor authentication
|
||||
can_create_events boolean
|
||||
can_change_teams boolean
|
||||
can_change_organizer_settings boolean
|
||||
can_manage_customers boolean
|
||||
can_manage_reusable_media boolean
|
||||
can_manage_gift_cards boolean
|
||||
can_change_event_settings boolean
|
||||
can_change_items boolean
|
||||
can_view_orders boolean
|
||||
can_change_orders boolean
|
||||
can_view_vouchers boolean
|
||||
can_change_vouchers boolean
|
||||
can_checkin_orders boolean
|
||||
all_event_permissions bool Whether members of this team are granted all event-level
|
||||
permissions, including future additions
|
||||
limit_event_permissions list of strings The event-level permissions team members are granted
|
||||
all_organizer_permissions bool Whether members of this team are granted all organizer-level
|
||||
permissions, including future additions
|
||||
all_organizer_permissions list of strings The organizer-level permissions team members are granted
|
||||
can_create_events boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``.
|
||||
can_change_teams boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``.
|
||||
can_change_organizer_settings boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``.
|
||||
can_manage_customers boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``.
|
||||
can_manage_reusable_media boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``.
|
||||
can_manage_gift_cards boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``.
|
||||
can_change_event_settings boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``.
|
||||
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
|
||||
|
||||
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
|
||||
--------------------
|
||||
|
||||
@@ -121,6 +157,10 @@ Team endpoints
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
"require_2fa": true,
|
||||
"all_event_permissions": true,
|
||||
"limit_event_permissions": [],
|
||||
"all_organizer_permissions": true,
|
||||
"limit_organizer_permissions": [],
|
||||
"can_create_events": true,
|
||||
...
|
||||
}
|
||||
@@ -159,6 +199,10 @@ Team endpoints
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
"require_2fa": true,
|
||||
"all_event_permissions": true,
|
||||
"limit_event_permissions": [],
|
||||
"all_organizer_permissions": true,
|
||||
"limit_organizer_permissions": [],
|
||||
"can_create_events": true,
|
||||
...
|
||||
}
|
||||
@@ -187,7 +231,10 @@ Team endpoints
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
"require_2fa": true,
|
||||
"can_create_events": true,
|
||||
"all_event_permissions": true,
|
||||
"limit_event_permissions": [],
|
||||
"all_organizer_permissions": true,
|
||||
"limit_organizer_permissions": [],
|
||||
...
|
||||
}
|
||||
|
||||
@@ -205,6 +252,10 @@ Team endpoints
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
"require_2fa": true,
|
||||
"all_event_permissions": true,
|
||||
"limit_event_permissions": [],
|
||||
"all_organizer_permissions": true,
|
||||
"limit_organizer_permissions": [],
|
||||
"can_create_events": true,
|
||||
...
|
||||
}
|
||||
@@ -232,7 +283,8 @@ Team endpoints
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"can_create_events": true
|
||||
"all_organizer_permissions": false,
|
||||
"limit_organizer_permissions": ["organizer.events:create"]
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -249,6 +301,10 @@ Team endpoints
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
"require_2fa": true,
|
||||
"all_event_permissions": true,
|
||||
"limit_event_permissions": [],
|
||||
"all_organizer_permissions": false,
|
||||
"limit_organizer_permissions": ["organizer.events:create"],
|
||||
"can_create_events": true,
|
||||
...
|
||||
}
|
||||
|
||||
@@ -55,12 +55,12 @@ your views:
|
||||
)
|
||||
|
||||
class AdminView(EventPermissionRequiredMixin, View):
|
||||
permission = 'can_view_orders'
|
||||
permission = 'event.orders:read'
|
||||
|
||||
...
|
||||
|
||||
|
||||
@event_permission_required('can_view_orders')
|
||||
@event_permission_required('event.orders:read')
|
||||
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')
|
||||
def navbar_info(sender, request, **kwargs):
|
||||
url = resolve(request.path_info)
|
||||
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_vouchers'):
|
||||
if not request.user.has_event_permission(request.organizer, request.event, 'event.vouchers:read'):
|
||||
return []
|
||||
return [{
|
||||
'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):
|
||||
model = Event
|
||||
permission = 'can_change_settings'
|
||||
permission = 'event.settings.general:write'
|
||||
form_class = MySettingsForm
|
||||
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
|
||||
.event`` and ``request.organizer`` are available as usual.
|
||||
|
||||
To require a special permission like ``can_view_orders``, you do not need to inherit from a special ViewSet base
|
||||
To require a special permission like ``event.orders:read``, you do not need to inherit from a special ViewSet base
|
||||
class, you can just set the ``permission`` attribute on your viewset:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyViewSet(ModelViewSet):
|
||||
permission = 'can_view_orders'
|
||||
permission = 'event.orders:read'
|
||||
...
|
||||
|
||||
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
|
||||
|
||||
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken) else request.user)
|
||||
if perm_holder.has_event_permission(request.event.organizer, request.event, 'can_view_orders'):
|
||||
if perm_holder.has_event_permission(request.event.organizer, request.event, 'event.orders:read'):
|
||||
...
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ Core
|
||||
: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,
|
||||
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_permissions, register_organizer_permissions
|
||||
|
||||
Order events
|
||||
""""""""""""
|
||||
|
||||
@@ -196,7 +196,7 @@ A simple implementation could look like this:
|
||||
.. code-block:: python
|
||||
|
||||
class MyNotificationType(NotificationType):
|
||||
required_permission = "can_view_orders"
|
||||
required_permission = "event.orders:read"
|
||||
action_type = "pretix.event.order.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.
|
||||
The central concept here is the concept of *Teams*. You can read more on `configuring teams and permissions <user-teams>`_
|
||||
The central concept here is the concept of *Teams*. You can read more on `configuring teams and permissions`_
|
||||
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
|
||||
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):
|
||||
permission = 'can_change_organizer_settings'
|
||||
# Only users with the permission ``can_change_organizer_settings`` on
|
||||
permission = 'organizer.settings.general:write'
|
||||
# Only users with the permission ``organizer.settings.general:write`` on
|
||||
# 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
|
||||
|
||||
|
||||
@organizer_permission_required('can_change_organizer_settings')
|
||||
@organizer_permission_required('organizer.settings.general:write')
|
||||
def my_orga_view(request, organizer, **kwargs):
|
||||
# Only users with the permission ``can_change_organizer_settings`` on
|
||||
# Only users with the permission ``organizer.settings.general:write`` on
|
||||
# this organizer can access this
|
||||
|
||||
|
||||
@@ -56,8 +56,8 @@ Of course, the same is available on event level:
|
||||
|
||||
|
||||
class MyEventView(EventPermissionRequiredMixin, View):
|
||||
permission = 'can_change_event_settings'
|
||||
# Only users with the permission ``can_change_event_settings`` on
|
||||
permission = 'event.settings.general:write'
|
||||
# Only users with the permission ``event.settings.general:write`` on
|
||||
# this event can access this
|
||||
|
||||
|
||||
@@ -66,9 +66,9 @@ Of course, the same is available on event level:
|
||||
# Only users with *any* permission on this event can access this
|
||||
|
||||
|
||||
@event_permission_required('can_change_event_settings')
|
||||
@event_permission_required('event.settings.general:write')
|
||||
def my_event_view(request, organizer, **kwargs):
|
||||
# Only users with the permission ``can_change_event_settings`` on
|
||||
# Only users with the permission ``event.settings.general:write`` on
|
||||
# this event can access this
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ When creating your own ``viewset`` using Django REST framework, you just need to
|
||||
and pretix will check it automatically for you::
|
||||
|
||||
class MyModelViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
permission = 'can_view_orders'
|
||||
permission = 'event.orders:read'
|
||||
|
||||
Checking permission in code
|
||||
---------------------------
|
||||
@@ -136,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::
|
||||
|
||||
>>> event.get_users_with_permission('can_change_event_settings')
|
||||
>>> event.get_users_with_permission('event.orders:read')
|
||||
<QuerySet: …>
|
||||
|
||||
Determine if a user has a certain permission for a specific event::
|
||||
|
||||
>>> user.has_event_permission(organizer, event, 'can_change_event_settings', request=request)
|
||||
>>> user.has_event_permission(organizer, event, 'event.orders:read', request=request)
|
||||
True
|
||||
|
||||
Determine if a user has any permission for a specific event::
|
||||
@@ -153,27 +153,27 @@ In the two previous commands, the ``request`` argument is optional, but required
|
||||
|
||||
The same method exists for organizer-level permissions::
|
||||
|
||||
>>> user.has_organizer_permission(organizer, 'can_change_event_settings', request=request)
|
||||
>>> user.has_organizer_permission(organizer, 'event.orders:read', request=request)
|
||||
True
|
||||
|
||||
Sometimes, it might be more useful to get the set of permissions at once::
|
||||
|
||||
>>> user.get_event_permission_set(organizer, event)
|
||||
{'can_change_event_settings', 'can_view_orders', 'can_change_orders'}
|
||||
{'event.settings.general:write', 'event.orders:read', 'event.orders:write'}
|
||||
|
||||
>>> user.get_organizer_permission_set(organizer, event)
|
||||
{'can_change_organizer_settings', 'can_create_events'}
|
||||
{'organizer.settings.general:write', 'organizer.events:create'}
|
||||
|
||||
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::
|
||||
|
||||
{% if "can_change_orders" in request.eventpermset %}
|
||||
{% if "event.orders:write" in request.eventpermset %}
|
||||
…
|
||||
{% endif %}
|
||||
|
||||
You can also do the reverse to get any events a user has access to::
|
||||
|
||||
>>> user.get_events_with_permission('can_change_event_settings', request=request)
|
||||
>>> user.get_events_with_permission('event.settings.general:write', request=request)
|
||||
<QuerySet: …>
|
||||
|
||||
>>> user.get_events_with_any_permission(request=request)
|
||||
@@ -195,3 +195,30 @@ 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,
|
||||
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.
|
||||
|
||||
Adding permissions
|
||||
------------------
|
||||
|
||||
Plugins can add permissions through the ``register_event_permissions`` and ``register_organizer_permission``.
|
||||
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.
|
||||
We recommend to prefix the permission string with the plugin name and follow the ``<module>.<thing>:<action>`` pattern.
|
||||
|
||||
Example::
|
||||
|
||||
@receiver(register_event_permissions)
|
||||
def register_default_event_permissions(sender, **kwargs):
|
||||
return [
|
||||
Permission("pretix_myplugin.resource:read", _("Read resources"),
|
||||
"pretix_myplugin", _("Some helptext")),
|
||||
]
|
||||
|
||||
|
||||
@receiver(register_organizer_permissions)
|
||||
def register_default_organizer_permissions(sender, **kwargs):
|
||||
return [
|
||||
Permission("pretix_myplugin.resource:read", _("Read resources"),
|
||||
"pretix_myplugin", _("Some helptext")),
|
||||
]
|
||||
|
||||
.. _configuring teams and permissions: https://docs.pretix.eu/guides/teams/
|
||||
@@ -1,9 +1,8 @@
|
||||
sphinx==7.4.*
|
||||
jinja2==3.1.*
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib-httpdomain
|
||||
sphinxcontrib-images
|
||||
sphinxcontrib-jquery
|
||||
sphinxcontrib-spelling==8.*
|
||||
sphinxemoji
|
||||
sphinx==9.1.*
|
||||
sphinx-rtd-theme~=3.1.0rc1
|
||||
sphinxcontrib-httpdomain~=1.8.1
|
||||
sphinxcontrib-images~=1.0.1
|
||||
sphinxcontrib-jquery~=4.1
|
||||
sphinxcontrib-spelling~=8.0.2
|
||||
sphinxemoji~=0.3.2
|
||||
pyenchant==3.3.*
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
-e ../
|
||||
sphinx==7.4.*
|
||||
jinja2==3.1.*
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib-httpdomain
|
||||
sphinxcontrib-images
|
||||
sphinxcontrib-jquery
|
||||
sphinxcontrib-spelling==8.*
|
||||
sphinxemoji
|
||||
sphinx==9.1.*
|
||||
sphinx-rtd-theme~=3.1.0rc1
|
||||
sphinxcontrib-httpdomain~=1.8.1
|
||||
sphinxcontrib-images~=1.0.1
|
||||
sphinxcontrib-jquery~=4.1
|
||||
sphinxcontrib-spelling~=8.0.2
|
||||
sphinxemoji~=0.3.2
|
||||
pyenchant==3.3.*
|
||||
|
||||
@@ -33,13 +33,13 @@ dependencies = [
|
||||
"celery==5.6.*",
|
||||
"chardet==5.2.*",
|
||||
"cryptography>=44.0.0",
|
||||
"css-inline==0.18.*",
|
||||
"css-inline==0.19.*",
|
||||
"defusedcsv>=1.1.0",
|
||||
"dnspython==2.*",
|
||||
"Django[argon2]==4.2.*,>=4.2.26",
|
||||
"django-bootstrap3==25.2",
|
||||
"django-compressor==4.5.1",
|
||||
"django-countries==7.6.*",
|
||||
"django-countries==8.2.*",
|
||||
"django-filter==25.1",
|
||||
"django-formset-js-improved==0.5.0.4",
|
||||
"django-formtools==2.5.1",
|
||||
@@ -51,7 +51,7 @@ dependencies = [
|
||||
"django-markup",
|
||||
"django-oauth-toolkit==2.3.*",
|
||||
"django-otp==1.6.*",
|
||||
"django-phonenumber-field==7.3.*",
|
||||
"django-phonenumber-field==8.3.*",
|
||||
"django-redis==6.0.*",
|
||||
"django-scopes==2.0.*",
|
||||
"django-statici18n==2.6.*",
|
||||
@@ -82,17 +82,17 @@ dependencies = [
|
||||
"pycountry",
|
||||
"pycparser==2.23",
|
||||
"pycryptodome==3.23.*",
|
||||
"pypdf==6.4.*",
|
||||
"pypdf==6.5.*",
|
||||
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
||||
"python-dateutil==2.9.*",
|
||||
"pytz",
|
||||
"pytz-deprecation-shim==0.1.*",
|
||||
"pyuca",
|
||||
"qrcode==8.2",
|
||||
"redis==6.4.*",
|
||||
"redis==7.0.*",
|
||||
"reportlab==4.4.*",
|
||||
"requests==2.32.*",
|
||||
"sentry-sdk==2.47.*",
|
||||
"sentry-sdk==2.48.*",
|
||||
"sepaxml==2.7.*",
|
||||
"stripe==7.9.*",
|
||||
"text-unidecode==1.*",
|
||||
@@ -110,7 +110,7 @@ dev = [
|
||||
"aiohttp==3.13.*",
|
||||
"coverage",
|
||||
"coveralls",
|
||||
"fakeredis==2.32.*",
|
||||
"fakeredis==2.33.*",
|
||||
"flake8==7.3.*",
|
||||
"freezegun",
|
||||
"isort==6.1.*",
|
||||
|
||||
@@ -124,12 +124,12 @@ class EventCRUDPermission(EventPermission):
|
||||
def has_permission(self, request, view):
|
||||
if not super(EventCRUDPermission, self).has_permission(request, view):
|
||||
return False
|
||||
elif view.action == 'create' and 'can_create_events' not in request.orgapermset:
|
||||
elif view.action == 'create' and 'organizer.events:create' not in request.orgapermset:
|
||||
return False
|
||||
elif view.action == 'destroy' and 'can_change_event_settings' not in request.eventpermset:
|
||||
elif view.action == 'destroy' and 'event.settings.general:write' not in request.eventpermset:
|
||||
return False
|
||||
elif view.action in ['update', 'partial_update'] \
|
||||
and 'can_change_event_settings' not in request.eventpermset:
|
||||
and 'event.settings.general:write' not in request.eventpermset:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -300,7 +300,7 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
def ignored_meta_properties(self):
|
||||
perm_holder = (self.context['request'].auth if isinstance(self.context['request'].auth, (Device, TeamAPIToken))
|
||||
else self.context['request'].user)
|
||||
if perm_holder.has_organizer_permission(self.context['request'].organizer, 'can_change_organizer_settings', request=self.context['request']):
|
||||
if perm_holder.has_organizer_permission(self.context['request'].organizer, 'organizer.settings.general:write', request=self.context['request']):
|
||||
return []
|
||||
return [k for k, p in self.meta_properties.items() if p.protected]
|
||||
|
||||
@@ -561,7 +561,7 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
def ignored_meta_properties(self):
|
||||
perm_holder = (self.context['request'].auth if isinstance(self.context['request'].auth, (Device, TeamAPIToken))
|
||||
else self.context['request'].user)
|
||||
if perm_holder.has_organizer_permission(self.context['request'].organizer, 'can_change_organizer_settings', request=self.context['request']):
|
||||
if perm_holder.has_organizer_permission(self.context['request'].organizer, 'organizer.settings.general:write', request=self.context['request']):
|
||||
return []
|
||||
return [k for k, p in self.meta_properties.items() if p.protected]
|
||||
|
||||
@@ -707,7 +707,10 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
||||
|
||||
|
||||
class EventSettingsSerializer(SettingsSerializer):
|
||||
default_write_permission = 'event.settings.general:write'
|
||||
default_fields = [
|
||||
# These are readable for all users with access to the events, therefore secrets made in the settings store
|
||||
# should not be included!
|
||||
'imprint_url',
|
||||
'checkout_email_helptext',
|
||||
'presale_has_ended_text',
|
||||
@@ -1079,16 +1082,16 @@ class SeatSerializer(I18nAwareModelSerializer):
|
||||
|
||||
def prefetch_expanded_data(self, items, request, expand_fields):
|
||||
if 'orderposition' in expand_fields:
|
||||
if 'can_view_orders' not in request.eventpermset:
|
||||
raise PermissionDenied('can_view_orders permission required for expand=orderposition')
|
||||
if 'event.orders:read' not in request.eventpermset:
|
||||
raise PermissionDenied('event.orders:read permission required for expand=orderposition')
|
||||
prefetch_by_id(items, OrderPosition.objects.prefetch_related('order'), 'orderposition_id', 'orderposition')
|
||||
if 'cartposition' in expand_fields:
|
||||
if 'can_view_orders' not in request.eventpermset:
|
||||
raise PermissionDenied('can_view_orders permission required for expand=cartposition')
|
||||
if 'event.orders:read' not in request.eventpermset:
|
||||
raise PermissionDenied('event.orders:read permission required for expand=cartposition')
|
||||
prefetch_by_id(items, CartPosition.objects, 'cartposition_id', 'cartposition')
|
||||
if 'voucher' in expand_fields:
|
||||
if 'can_view_vouchers' not in request.eventpermset:
|
||||
raise PermissionDenied('can_view_vouchers permission required for expand=voucher')
|
||||
if 'event.vouchers:read' not in request.eventpermset:
|
||||
raise PermissionDenied('event.vouchers:read permission required for expand=voucher')
|
||||
prefetch_by_id(items, Voucher.objects, 'voucher_id', 'voucher')
|
||||
|
||||
def __init__(self, instance, *args, **kwargs):
|
||||
|
||||
@@ -24,7 +24,7 @@ from decimal import Decimal
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.order import OrderPositionSerializer
|
||||
@@ -66,6 +66,9 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
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)
|
||||
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)
|
||||
@@ -77,6 +80,8 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
||||
)
|
||||
|
||||
if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'):
|
||||
# No additional permission check performed, documented limitation of the permission system
|
||||
# Would get to complex/unusable otherwise since the permission depends on the event
|
||||
self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True)
|
||||
else:
|
||||
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
|
||||
@@ -86,6 +91,9 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
||||
)
|
||||
|
||||
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)
|
||||
else:
|
||||
self.fields['customer'] = serializers.SlugRelatedField(
|
||||
|
||||
@@ -613,7 +613,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
# /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
|
||||
# layer to not set pdf_data=true in the first place.
|
||||
request and hasattr(request, 'eventpermset') and 'can_view_orders' not in request.eventpermset
|
||||
request and hasattr(request, 'eventpermset') and 'event.orders:read' not in request.eventpermset
|
||||
)
|
||||
if ('pdf_data' in self.context and not self.context['pdf_data']) or pdf_data_forbidden:
|
||||
self.fields.pop('pdf_data', None)
|
||||
|
||||
@@ -33,7 +33,7 @@ from pretix.api.serializers.order import (
|
||||
OrderFeeCreateSerializer, OrderPositionCreateSerializer,
|
||||
)
|
||||
from pretix.base.models import ItemVariation, Order, OrderFee, OrderPosition
|
||||
from pretix.base.services.orders import OrderError
|
||||
from pretix.base.services.orders import OrderChangeManager, OrderError
|
||||
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -82,11 +82,11 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
ocm = self.context['ocm']
|
||||
ocm: OrderChangeManager = self.context['ocm']
|
||||
check_quotas = self.context.get('check_quotas', True)
|
||||
|
||||
try:
|
||||
ocm.add_position(
|
||||
new_position = ocm.add_position(
|
||||
item=validated_data['item'],
|
||||
variation=validated_data.get('variation'),
|
||||
price=validated_data.get('price'),
|
||||
@@ -98,7 +98,7 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
|
||||
)
|
||||
if self.context.get('commit', True):
|
||||
ocm.commit(check_quotas=check_quotas)
|
||||
return validated_data['order'].positions.order_by('-positionid').first()
|
||||
return new_position.position
|
||||
else:
|
||||
return OrderPosition() # fake to appease DRF
|
||||
except OrderError as e:
|
||||
@@ -131,7 +131,7 @@ class OrderFeeCreateForExistingOrderSerializer(OrderFeeCreateSerializer):
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
ocm = self.context['ocm']
|
||||
ocm: OrderChangeManager = self.context['ocm']
|
||||
|
||||
try:
|
||||
f = OrderFee(
|
||||
@@ -146,7 +146,7 @@ class OrderFeeCreateForExistingOrderSerializer(OrderFeeCreateSerializer):
|
||||
ocm.add_fee(f)
|
||||
if self.context.get('commit', True):
|
||||
ocm.commit()
|
||||
return validated_data['order'].fees.order_by('-pk').first()
|
||||
return f
|
||||
else:
|
||||
return OrderFee() # fake to appease DRF
|
||||
except OrderError as e:
|
||||
@@ -310,7 +310,7 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer):
|
||||
return data
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
ocm = self.context['ocm']
|
||||
ocm: OrderChangeManager = self.context['ocm']
|
||||
check_quotas = self.context.get('check_quotas', True)
|
||||
current_seat = {'seat_guid': instance.seat.seat_guid} if instance.seat else None
|
||||
item = validated_data.get('item', instance.item)
|
||||
@@ -399,7 +399,7 @@ class OrderFeeChangeSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
ocm = self.context['ocm']
|
||||
ocm: OrderChangeManager = self.context['ocm']
|
||||
value = validated_data.get('value', instance.value)
|
||||
|
||||
try:
|
||||
|
||||
@@ -45,12 +45,19 @@ from pretix.base.models import (
|
||||
SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||
)
|
||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||
from pretix.base.permissions import (
|
||||
get_all_event_permissions, get_all_organizer_permissions,
|
||||
)
|
||||
from pretix.base.plugins import (
|
||||
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
||||
PLUGIN_LEVEL_ORGANIZER,
|
||||
)
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
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.multidomain.urlreverse import build_absolute_uri
|
||||
|
||||
@@ -306,21 +313,97 @@ class EventSlugField(serializers.SlugRelatedField):
|
||||
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):
|
||||
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:
|
||||
model = Team
|
||||
fields = (
|
||||
'id', 'name', 'require_2fa', 'all_events', 'limit_events', 'can_create_events', 'can_change_teams',
|
||||
'can_change_organizer_settings', 'can_manage_gift_cards', 'can_change_event_settings',
|
||||
'can_change_items', 'can_view_orders', 'can_change_orders', 'can_view_vouchers',
|
||||
'can_change_vouchers', 'can_checkin_orders', 'can_manage_customers', 'can_manage_reusable_media'
|
||||
'id', 'name', 'require_2fa', 'all_events', 'limit_events', 'all_event_permissions', 'limit_event_permissions',
|
||||
'all_organizer_permissions', 'limit_organizer_permissions', 'can_change_event_settings',
|
||||
'can_change_items', 'can_view_orders', 'can_change_orders', 'can_checkin_orders', 'can_view_vouchers',
|
||||
'can_change_vouchers', 'can_create_events', 'can_change_organizer_settings', 'can_change_teams',
|
||||
'can_manage_gift_cards', 'can_manage_customers', 'can_manage_reusable_media'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['limit_event_permissions'].choices = [(p.name, p.name) for p in get_all_event_permissions().values()]
|
||||
self.fields['limit_organizer_permissions'].choices = [(p.name, p.name) for p in get_all_organizer_permissions().values()]
|
||||
|
||||
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):
|
||||
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.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_plugable_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'):
|
||||
raise ValidationError('Do not set both limit_events and all_events.')
|
||||
return data
|
||||
@@ -339,7 +422,7 @@ class DeviceSerializer(serializers.ModelSerializer):
|
||||
created = serializers.DateTimeField(read_only=True)
|
||||
revoked = serializers.BooleanField(read_only=True)
|
||||
initialized = serializers.DateTimeField(read_only=True)
|
||||
initialization_token = serializers.DateTimeField(read_only=True)
|
||||
initialization_token = serializers.CharField(read_only=True)
|
||||
security_profile = serializers.ChoiceField(choices=[], required=False, default="full")
|
||||
|
||||
class Meta:
|
||||
@@ -353,6 +436,8 @@ class DeviceSerializer(serializers.ModelSerializer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
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):
|
||||
@@ -439,7 +524,10 @@ class TeamMemberSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class OrganizerSettingsSerializer(SettingsSerializer):
|
||||
default_write_permission = 'organizer.settings.general:write'
|
||||
default_fields = [
|
||||
# These are readable for all users with access to the events, therefore secrets made in the settings store
|
||||
# should not be included!
|
||||
'customer_accounts',
|
||||
'customer_accounts_native',
|
||||
'customer_accounts_link_by_email',
|
||||
|
||||
@@ -37,6 +37,8 @@ logger = logging.getLogger(__name__)
|
||||
class SettingsSerializer(serializers.Serializer):
|
||||
default_fields = []
|
||||
readonly_fields = []
|
||||
default_write_permission = 'organizer.settings.general:write'
|
||||
write_permission_required = {}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.changed_data = []
|
||||
@@ -58,9 +60,17 @@ class SettingsSerializer(serializers.Serializer):
|
||||
f._label = str(form_kwargs.get('label', fname))
|
||||
f._help_text = str(form_kwargs.get('help_text'))
|
||||
f.parent = self
|
||||
|
||||
self.write_permission_required[fname] = DEFAULTS[fname].get('write_permission', self.default_write_permission)
|
||||
|
||||
self.fields[fname] = f
|
||||
|
||||
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}
|
||||
|
||||
def update(self, instance: HierarkeyProxy, validated_data):
|
||||
|
||||
@@ -52,8 +52,8 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
|
||||
ordering = ('datetime',)
|
||||
ordering_fields = ('datetime', 'cart_id')
|
||||
lookup_field = 'id'
|
||||
permission = 'can_view_orders'
|
||||
write_permission = 'can_change_orders'
|
||||
permission = 'event.orders:read'
|
||||
write_permission = 'event.orders:write'
|
||||
|
||||
def get_queryset(self):
|
||||
return CartPosition.objects.filter(
|
||||
|
||||
@@ -118,11 +118,11 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
|
||||
def _get_permission_name(self, request):
|
||||
if request.path.endswith('/failed_checkins/'):
|
||||
return 'can_checkin_orders', 'can_change_orders'
|
||||
return 'event.orders:checkin', 'event.orders:write'
|
||||
elif request.method in SAFE_METHODS:
|
||||
return 'can_view_orders', 'can_checkin_orders',
|
||||
return 'event.orders:read', 'event.orders:checkin',
|
||||
else:
|
||||
return 'can_change_event_settings'
|
||||
return 'event.settings.general:write'
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.event.checkin_lists.prefetch_related(
|
||||
@@ -457,7 +457,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'event': op.order.event,
|
||||
'pdf_data': pdf_data and (
|
||||
user if user and user.is_authenticated else auth
|
||||
).has_event_permission(request.organizer, event, 'can_view_orders', request),
|
||||
).has_event_permission(request.organizer, event, 'event.orders:read', request),
|
||||
}
|
||||
|
||||
common_checkin_args = dict(
|
||||
@@ -822,8 +822,8 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
}
|
||||
|
||||
filterset_class = CheckinOrderPositionFilter
|
||||
permission = ('can_view_orders', 'can_checkin_orders')
|
||||
write_permission = ('can_change_orders', 'can_checkin_orders')
|
||||
permission = ('event.orders:read', 'event.orders:checkin')
|
||||
write_permission = ('event.orders:write', 'event.orders:checkin')
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
@@ -854,7 +854,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
expand=self.request.query_params.getlist('expand'),
|
||||
)
|
||||
|
||||
if 'pk' not in self.request.resolver_match.kwargs and 'can_view_orders' not in self.request.eventpermset \
|
||||
if 'pk' not in self.request.resolver_match.kwargs and 'event.orders:read' not in self.request.eventpermset \
|
||||
and len(self.request.query_params.get('search', '')) < 3:
|
||||
qs = qs.none()
|
||||
|
||||
@@ -903,9 +903,9 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class CheckinRPCRedeemView(views.APIView):
|
||||
def post(self, request, *args, **kwargs):
|
||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||
events = self.request.auth.get_events_with_permission(('can_change_orders', 'can_checkin_orders'))
|
||||
events = self.request.auth.get_events_with_permission(('event.orders:write', 'event.orders:checkin'))
|
||||
elif self.request.user.is_authenticated:
|
||||
events = self.request.user.get_events_with_permission(('can_change_orders', 'can_checkin_orders'), self.request).filter(
|
||||
events = self.request.user.get_events_with_permission(('event.orders:write', 'event.orders:checkin'), self.request).filter(
|
||||
organizer=self.request.organizer
|
||||
)
|
||||
else:
|
||||
@@ -972,9 +972,9 @@ class CheckinRPCSearchView(ListAPIView):
|
||||
@cached_property
|
||||
def lists(self):
|
||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||
events = self.request.auth.get_events_with_permission(('can_view_orders', 'can_checkin_orders'))
|
||||
events = self.request.auth.get_events_with_permission(('event.orders:read', 'event.orders:checkin'))
|
||||
elif self.request.user.is_authenticated:
|
||||
events = self.request.user.get_events_with_permission(('can_view_orders', 'can_checkin_orders'), self.request).filter(
|
||||
events = self.request.user.get_events_with_permission(('event.orders:read', 'event.orders:checkin'), self.request).filter(
|
||||
organizer=self.request.organizer
|
||||
)
|
||||
else:
|
||||
@@ -991,9 +991,9 @@ class CheckinRPCSearchView(ListAPIView):
|
||||
@cached_property
|
||||
def has_full_access_permission(self):
|
||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||
events = self.request.auth.get_events_with_permission('can_view_orders')
|
||||
events = self.request.auth.get_events_with_permission('event.orders:read')
|
||||
elif self.request.user.is_authenticated:
|
||||
events = self.request.user.get_events_with_permission('can_view_orders', self.request).filter(
|
||||
events = self.request.user.get_events_with_permission('event.orders:read', self.request).filter(
|
||||
organizer=self.request.organizer
|
||||
)
|
||||
else:
|
||||
@@ -1020,9 +1020,9 @@ class CheckinRPCSearchView(ListAPIView):
|
||||
class CheckinRPCAnnulView(views.APIView):
|
||||
def post(self, request, *args, **kwargs):
|
||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||
events = self.request.auth.get_events_with_permission(('can_change_orders', 'can_checkin_orders'))
|
||||
events = self.request.auth.get_events_with_permission(('event.orders:write', 'event.orders:checkin'))
|
||||
elif self.request.user.is_authenticated:
|
||||
events = self.request.user.get_events_with_permission(('can_change_orders', 'can_checkin_orders'), self.request).filter(
|
||||
events = self.request.user.get_events_with_permission(('event.orders:write', 'event.orders:checkin'), self.request).filter(
|
||||
organizer=self.request.organizer
|
||||
)
|
||||
else:
|
||||
@@ -1100,7 +1100,7 @@ class CheckinViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
filterset_class = CheckinFilter
|
||||
ordering = ('created', 'id')
|
||||
ordering_fields = ('created', 'datetime', 'id',)
|
||||
permission = 'can_view_orders'
|
||||
permission = 'event.orders:read'
|
||||
|
||||
def get_queryset(self):
|
||||
qs = Checkin.all.filter().select_related(
|
||||
|
||||
@@ -57,7 +57,7 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('position', 'id')
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
write_permission = 'event.items:write'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.discounts.prefetch_related(
|
||||
|
||||
@@ -341,7 +341,7 @@ class CloneEventViewSet(viewsets.ModelViewSet):
|
||||
lookup_field = 'slug'
|
||||
lookup_url_kwarg = 'event'
|
||||
http_method_names = ['post']
|
||||
write_permission = 'can_create_events'
|
||||
write_permission = 'event.settings.general:write'
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
@@ -350,6 +350,12 @@ class CloneEventViewSet(viewsets.ModelViewSet):
|
||||
return ctx
|
||||
|
||||
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")
|
||||
|
||||
serializer.save(organizer=self.request.organizer)
|
||||
|
||||
serializer.instance.log_action(
|
||||
@@ -426,7 +432,7 @@ with scopes_disabled():
|
||||
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = SubEventSerializer
|
||||
queryset = SubEvent.objects.none()
|
||||
write_permission = 'can_change_event_settings'
|
||||
write_permission = 'event.subevents:write'
|
||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
||||
ordering = ('date_from',)
|
||||
ordering_fields = ('id', 'date_from', 'last_modified')
|
||||
@@ -546,7 +552,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = TaxRuleSerializer
|
||||
queryset = TaxRule.objects.none()
|
||||
write_permission = 'can_change_event_settings'
|
||||
write_permission = 'event.settings.tax:write'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.tax_rules.all()
|
||||
@@ -589,7 +595,7 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
class ItemMetaPropertiesViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ItemMetaPropertiesSerializer
|
||||
queryset = ItemMetaProperty.objects.none()
|
||||
write_permission = 'can_change_event_settings'
|
||||
write_permission = 'event.settings.general:write'
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.event.item_meta_properties.all()
|
||||
@@ -636,19 +642,18 @@ class ItemMetaPropertiesViewSet(viewsets.ModelViewSet):
|
||||
|
||||
class EventSettingsView(views.APIView):
|
||||
permission = None
|
||||
write_permission = 'can_change_event_settings'
|
||||
write_permission = 'event.settings.general:write'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if isinstance(request.auth, Device):
|
||||
s = DeviceEventSettingsSerializer(instance=request.event.settings, event=request.event, context={
|
||||
'request': request
|
||||
})
|
||||
elif 'can_change_event_settings' in request.eventpermset:
|
||||
s = EventSettingsSerializer(instance=request.event.settings, event=request.event, context={
|
||||
'request': request
|
||||
'request': request, 'permissions': request.eventpermset
|
||||
})
|
||||
else:
|
||||
raise PermissionDenied()
|
||||
s = EventSettingsSerializer(instance=request.event.settings, event=request.event, context={
|
||||
'request': request, 'permissions': request.eventpermset,
|
||||
})
|
||||
|
||||
if 'explain' in request.GET:
|
||||
return Response({
|
||||
fname: {
|
||||
@@ -662,7 +667,7 @@ class EventSettingsView(views.APIView):
|
||||
|
||||
def patch(self, request, *wargs, **kwargs):
|
||||
s = EventSettingsSerializer(instance=request.event.settings, data=request.data, partial=True,
|
||||
event=request.event, context={'request': request})
|
||||
event=request.event, context={'request': request, 'permissions': request.eventpermset})
|
||||
s.is_valid(raise_exception=True)
|
||||
with transaction.atomic():
|
||||
s.save()
|
||||
@@ -674,7 +679,7 @@ class EventSettingsView(views.APIView):
|
||||
)
|
||||
s = EventSettingsSerializer(
|
||||
instance=request.event.settings, event=request.event, context={
|
||||
'request': request
|
||||
'request': request, 'permissions': request.eventpermset
|
||||
})
|
||||
return Response(s.data)
|
||||
|
||||
@@ -701,7 +706,7 @@ class SeatFilter(FilterSet):
|
||||
class SeatViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = SeatSerializer
|
||||
queryset = Seat.objects.none()
|
||||
write_permission = 'can_change_event_settings'
|
||||
write_permission = 'event.settings.general:write'
|
||||
filter_backends = (DjangoFilterBackend, )
|
||||
filterset_class = SeatFilter
|
||||
|
||||
|
||||
@@ -74,6 +74,11 @@ class ExportersMixin:
|
||||
@action(detail=True, methods=['GET'], url_name='download', url_path='download/(?P<asyncid>[^/]+)/(?P<cfid>[^/]+)')
|
||||
def download(self, *args, **kwargs):
|
||||
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:
|
||||
resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename).encode("ascii", "ignore")
|
||||
@@ -109,7 +114,8 @@ class ExportersMixin:
|
||||
serializer = JobRunSerializer(exporter=instance, data=self.request.data, **self.get_serializer_kwargs())
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
cf = CachedFile(web_download=False)
|
||||
cf = CachedFile(web_download=True)
|
||||
cf.bind_to_session(self.request, "exporters-api")
|
||||
cf.date = now()
|
||||
cf.expires = now() + timedelta(hours=24)
|
||||
cf.save()
|
||||
@@ -130,7 +136,7 @@ class ExportersMixin:
|
||||
|
||||
|
||||
class EventExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
||||
permission = 'can_view_orders'
|
||||
permission = 'event.orders:read'
|
||||
|
||||
def get_serializer_kwargs(self):
|
||||
return {}
|
||||
@@ -163,7 +169,7 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
||||
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(
|
||||
events = perm_holder.get_events_with_permission('event.orders:read', request=self.request).filter(
|
||||
organizer=self.request.organizer
|
||||
)
|
||||
responses = register_multievent_data_exporters.send(self.request.organizer)
|
||||
@@ -190,7 +196,7 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
||||
else:
|
||||
perm_holder = self.request.user
|
||||
return {
|
||||
'events': perm_holder.get_events_with_permission('can_view_orders', request=self.request).filter(
|
||||
'events': perm_holder.get_events_with_permission('event.orders:read', request=self.request).filter(
|
||||
organizer=self.request.organizer
|
||||
)
|
||||
}
|
||||
@@ -216,11 +222,11 @@ class ScheduledExportersViewSet(viewsets.ModelViewSet):
|
||||
class ScheduledEventExportViewSet(ScheduledExportersViewSet):
|
||||
serializer_class = ScheduledEventExportSerializer
|
||||
queryset = ScheduledEventExport.objects.none()
|
||||
permission = 'can_view_orders'
|
||||
permission = 'event.orders:read'
|
||||
|
||||
def get_queryset(self):
|
||||
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, 'can_change_event_settings',
|
||||
if not perm_holder.has_event_permission(self.request.organizer, self.request.event, 'event.settings.general:write',
|
||||
request=self.request):
|
||||
if self.request.user.is_authenticated:
|
||||
qs = self.request.event.scheduled_exports.filter(owner=self.request.user)
|
||||
@@ -285,7 +291,7 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
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, 'can_change_organizer_settings',
|
||||
if not perm_holder.has_organizer_permission(self.request.organizer, 'organizer.settings.general:write',
|
||||
request=self.request):
|
||||
if self.request.user.is_authenticated:
|
||||
qs = self.request.organizer.scheduled_exports.filter(owner=self.request.user)
|
||||
@@ -318,9 +324,9 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
|
||||
@cached_property
|
||||
def events(self):
|
||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||
return self.request.auth.get_events_with_permission('can_view_orders')
|
||||
return self.request.auth.get_events_with_permission('event.orders:read')
|
||||
elif self.request.user.is_authenticated:
|
||||
return self.request.user.get_events_with_permission('can_view_orders', self.request).filter(
|
||||
return self.request.user.get_events_with_permission('event.orders:read', self.request).filter(
|
||||
organizer=self.request.organizer
|
||||
)
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
ordering = ('position', 'id')
|
||||
filterset_class = ItemFilter
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
write_permission = 'event.items:write'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.items.select_related('tax_rule').prefetch_related(
|
||||
@@ -163,7 +163,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('id',)
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
write_permission = 'event.items:write'
|
||||
|
||||
@cached_property
|
||||
def item(self):
|
||||
@@ -234,7 +234,7 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
|
||||
ordering_fields = ('id',)
|
||||
ordering = ('id',)
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
write_permission = 'event.items:write'
|
||||
|
||||
@cached_property
|
||||
def item(self):
|
||||
@@ -286,7 +286,7 @@ class ItemProgramTimeViewSet(viewsets.ModelViewSet):
|
||||
ordering_fields = ('id',)
|
||||
ordering = ('id',)
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
write_permission = 'event.items:write'
|
||||
|
||||
@cached_property
|
||||
def item(self):
|
||||
@@ -339,7 +339,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('id',)
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
write_permission = 'event.items:write'
|
||||
|
||||
@cached_property
|
||||
def item(self):
|
||||
@@ -398,7 +398,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('position', 'id')
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
write_permission = 'event.items:write'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.categories.all()
|
||||
@@ -453,7 +453,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('position', 'id')
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
write_permission = 'event.items:write'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.questions.prefetch_related('options').all()
|
||||
@@ -497,7 +497,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('position',)
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
write_permission = 'event.items:write'
|
||||
|
||||
def get_queryset(self):
|
||||
q = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
|
||||
@@ -564,7 +564,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
ordering_fields = ('id', 'size')
|
||||
ordering = ('id',)
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
write_permission = 'event.items:write'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.quotas.select_related('subevent').prefetch_related('items', 'variations').all()
|
||||
|
||||
@@ -62,8 +62,8 @@ with scopes_disabled():
|
||||
class ReusableMediaViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ReusableMediaSerializer
|
||||
queryset = ReusableMedium.objects.none()
|
||||
permission = 'can_manage_reusable_media'
|
||||
write_permission = 'can_manage_reusable_media'
|
||||
permission = 'organizer.reusablemedia:read'
|
||||
write_permission = 'organizer.reusablemedia:write'
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
ordering = ('-updated', '-id')
|
||||
ordering_fields = ('created', 'updated', 'identifier', 'type', 'id')
|
||||
@@ -95,6 +95,8 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
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
|
||||
|
||||
@transaction.atomic()
|
||||
|
||||
@@ -317,7 +317,7 @@ class OrderViewSetMixin:
|
||||
|
||||
class OrganizerOrderViewSet(OrderViewSetMixin, viewsets.ReadOnlyModelViewSet):
|
||||
def get_base_queryset(self):
|
||||
perm = "can_view_orders" if self.request.method in SAFE_METHODS else "can_change_orders"
|
||||
perm = "event.orders:read" if self.request.method in SAFE_METHODS else "event.orders:write"
|
||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||
return Order.objects.filter(
|
||||
event__organizer=self.request.organizer,
|
||||
@@ -338,8 +338,8 @@ class OrganizerOrderViewSet(OrderViewSetMixin, viewsets.ReadOnlyModelViewSet):
|
||||
|
||||
|
||||
class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
permission = 'can_view_orders'
|
||||
write_permission = 'can_change_orders'
|
||||
permission = 'event.orders:read'
|
||||
write_permission = 'event.orders:write'
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
@@ -1078,8 +1078,8 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
||||
ordering = ('order__datetime', 'positionid')
|
||||
ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',)
|
||||
filterset_class = OrderPositionFilter
|
||||
permission = 'can_view_orders'
|
||||
write_permission = 'can_change_orders'
|
||||
permission = 'event.orders:read'
|
||||
write_permission = 'event.orders:write'
|
||||
ordering_custom = {
|
||||
'attendee_name': {
|
||||
'_order': F('display_name').asc(nulls_first=True),
|
||||
@@ -1580,8 +1580,8 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
||||
class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = OrderPaymentSerializer
|
||||
queryset = OrderPayment.objects.none()
|
||||
permission = 'can_view_orders'
|
||||
write_permission = 'can_change_orders'
|
||||
permission = 'event.orders:read'
|
||||
write_permission = 'event.orders:write'
|
||||
lookup_field = 'local_id'
|
||||
|
||||
def get_serializer_context(self):
|
||||
@@ -1757,8 +1757,8 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = OrderRefundSerializer
|
||||
queryset = OrderRefund.objects.none()
|
||||
permission = 'can_view_orders'
|
||||
write_permission = 'can_change_orders'
|
||||
permission = 'event.orders:read'
|
||||
write_permission = 'event.orders:write'
|
||||
lookup_field = 'local_id'
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -1915,13 +1915,18 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
ordering = ('nr',)
|
||||
ordering_fields = ('nr', 'date')
|
||||
filterset_class = InvoiceFilter
|
||||
permission = 'can_view_orders'
|
||||
lookup_url_kwarg = 'number'
|
||||
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):
|
||||
perm = "can_view_orders" if self.request.method in SAFE_METHODS else "can_change_orders"
|
||||
perm = "event.orders:read" if self.request.method in SAFE_METHODS else "event.orders:write"
|
||||
if getattr(self.request, 'event', None):
|
||||
qs = self.request.event.invoices
|
||||
elif isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||
@@ -2062,8 +2067,8 @@ class RevokedSecretViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
ordering = ('-created',)
|
||||
ordering_fields = ('created', 'secret')
|
||||
filterset_class = RevokedSecretFilter
|
||||
permission = 'can_view_orders'
|
||||
write_permission = 'can_change_orders'
|
||||
permission = 'event.orders:read'
|
||||
write_permission = 'event.orders:write'
|
||||
|
||||
def get_queryset(self):
|
||||
return RevokedTicketSecret.objects.filter(event=self.request.event)
|
||||
@@ -2084,8 +2089,8 @@ class BlockedSecretViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
||||
ordering = ('-updated', '-pk')
|
||||
filterset_class = BlockedSecretFilter
|
||||
permission = 'can_view_orders'
|
||||
write_permission = 'can_change_orders'
|
||||
permission = 'event.orders:read'
|
||||
write_permission = 'event.orders:write'
|
||||
|
||||
def get_queryset(self):
|
||||
return BlockedTicketSecret.objects.filter(event=self.request.event)
|
||||
@@ -2120,7 +2125,7 @@ class TransactionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
ordering = ('datetime', 'pk')
|
||||
ordering_fields = ('datetime', 'created', 'id',)
|
||||
filterset_class = TransactionFilter
|
||||
permission = 'can_view_orders'
|
||||
permission = 'event.orders:read'
|
||||
|
||||
def get_queryset(self):
|
||||
return Transaction.objects.filter(order__event=self.request.event).select_related("order")
|
||||
@@ -2137,11 +2142,11 @@ class OrganizerTransactionViewSet(TransactionViewSet):
|
||||
|
||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||
qs = qs.filter(
|
||||
order__event__in=self.request.auth.get_events_with_permission("can_view_orders"),
|
||||
order__event__in=self.request.auth.get_events_with_permission("event.orders:read"),
|
||||
)
|
||||
elif self.request.user.is_authenticated:
|
||||
qs = qs.filter(
|
||||
order__event__in=self.request.user.get_events_with_permission("can_view_orders", request=self.request)
|
||||
order__event__in=self.request.user.get_events_with_permission("event.orders:read", request=self.request)
|
||||
)
|
||||
else:
|
||||
raise PermissionDenied("Unknown authentication scheme")
|
||||
|
||||
@@ -70,7 +70,7 @@ class OrganizerViewSet(mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
filter_backends = (TotalOrderingFilter,)
|
||||
ordering = ('slug',)
|
||||
ordering_fields = ('name', 'slug')
|
||||
write_permission = "can_change_organizer_settings"
|
||||
write_permission = "organizer.settings.general:write"
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.user.is_authenticated:
|
||||
@@ -154,8 +154,8 @@ class OrganizerViewSet(mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
class SeatingPlanViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = SeatingPlanSerializer
|
||||
queryset = SeatingPlan.objects.none()
|
||||
permission = 'can_change_organizer_settings'
|
||||
write_permission = 'can_change_organizer_settings'
|
||||
permission = None
|
||||
write_permission = 'organizer.seatingplans:write'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.seating_plans.order_by('name')
|
||||
@@ -221,8 +221,8 @@ with scopes_disabled():
|
||||
class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = GiftCardSerializer
|
||||
queryset = GiftCard.objects.none()
|
||||
permission = 'can_manage_gift_cards'
|
||||
write_permission = 'can_manage_gift_cards'
|
||||
permission = 'organizer.giftcards:read'
|
||||
write_permission = 'organizer.giftcards:write'
|
||||
filter_backends = (DjangoFilterBackend,)
|
||||
filterset_class = GiftCardFilter
|
||||
|
||||
@@ -323,8 +323,8 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
class GiftCardTransactionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = GiftCardTransactionSerializer
|
||||
queryset = GiftCardTransaction.objects.none()
|
||||
permission = 'can_manage_gift_cards'
|
||||
write_permission = 'can_manage_gift_cards'
|
||||
permission = 'organizer.giftcards:read'
|
||||
write_permission = 'organizer.giftcards:write'
|
||||
|
||||
@cached_property
|
||||
def giftcard(self):
|
||||
@@ -341,8 +341,8 @@ class GiftCardTransactionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class TeamViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = TeamSerializer
|
||||
queryset = Team.objects.none()
|
||||
permission = 'can_change_teams'
|
||||
write_permission = 'can_change_teams'
|
||||
permission = 'organizer.teams:write'
|
||||
write_permission = 'organizer.teams:write'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.teams.order_by('pk')
|
||||
@@ -381,8 +381,8 @@ class TeamViewSet(viewsets.ModelViewSet):
|
||||
class TeamMemberViewSet(DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = TeamMemberSerializer
|
||||
queryset = User.objects.none()
|
||||
permission = 'can_change_teams'
|
||||
write_permission = 'can_change_teams'
|
||||
permission = 'organizer.teams:write'
|
||||
write_permission = 'organizer.teams:write'
|
||||
|
||||
@cached_property
|
||||
def team(self):
|
||||
@@ -410,8 +410,8 @@ class TeamMemberViewSet(DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
class TeamInviteViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = TeamInviteSerializer
|
||||
queryset = TeamInvite.objects.none()
|
||||
permission = 'can_change_teams'
|
||||
write_permission = 'can_change_teams'
|
||||
permission = 'organizer.teams:write'
|
||||
write_permission = 'organizer.teams:write'
|
||||
|
||||
@cached_property
|
||||
def team(self):
|
||||
@@ -447,8 +447,8 @@ class TeamInviteViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyMo
|
||||
class TeamAPITokenViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = TeamAPITokenSerializer
|
||||
queryset = TeamAPIToken.objects.none()
|
||||
permission = 'can_change_teams'
|
||||
write_permission = 'can_change_teams'
|
||||
permission = 'organizer.teams:write'
|
||||
write_permission = 'organizer.teams:write'
|
||||
|
||||
@cached_property
|
||||
def team(self):
|
||||
@@ -511,8 +511,8 @@ class DeviceViewSet(mixins.CreateModelMixin,
|
||||
GenericViewSet):
|
||||
serializer_class = DeviceSerializer
|
||||
queryset = Device.objects.none()
|
||||
permission = 'can_change_organizer_settings'
|
||||
write_permission = 'can_change_organizer_settings'
|
||||
permission = 'organizer.devices:read'
|
||||
write_permission = 'organizer.devices:write'
|
||||
lookup_field = 'device_id'
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -521,6 +521,9 @@ class DeviceViewSet(mixins.CreateModelMixin,
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
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
|
||||
|
||||
@transaction.atomic()
|
||||
@@ -547,11 +550,11 @@ class DeviceViewSet(mixins.CreateModelMixin,
|
||||
|
||||
class OrganizerSettingsView(views.APIView):
|
||||
permission = None
|
||||
write_permission = 'can_change_organizer_settings'
|
||||
write_permission = 'organizer.settings.general:write'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
|
||||
'request': request
|
||||
'request': request, 'permissions': request.orgapermset
|
||||
})
|
||||
if 'explain' in request.GET:
|
||||
return Response({
|
||||
@@ -568,7 +571,7 @@ class OrganizerSettingsView(views.APIView):
|
||||
s = OrganizerSettingsSerializer(
|
||||
instance=request.organizer.settings, data=request.data, partial=True,
|
||||
organizer=request.organizer, context={
|
||||
'request': request
|
||||
'request': request, 'permissions': request.orgapermset
|
||||
}
|
||||
)
|
||||
s.is_valid(raise_exception=True)
|
||||
@@ -580,7 +583,7 @@ class OrganizerSettingsView(views.APIView):
|
||||
}
|
||||
)
|
||||
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
|
||||
'request': request
|
||||
'request': request, 'permissions': request.orgapermset
|
||||
})
|
||||
return Response(s.data)
|
||||
|
||||
@@ -597,7 +600,8 @@ with scopes_disabled():
|
||||
class CustomerViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = CustomerSerializer
|
||||
queryset = Customer.objects.none()
|
||||
permission = 'can_manage_customers'
|
||||
permission = 'organizer.customers:read'
|
||||
write_permission = 'organizer.customers:write'
|
||||
lookup_field = 'identifier'
|
||||
filter_backends = (DjangoFilterBackend,)
|
||||
filterset_class = CustomerFilter
|
||||
@@ -657,7 +661,7 @@ class CustomerViewSet(viewsets.ModelViewSet):
|
||||
class MembershipTypeViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = MembershipTypeSerializer
|
||||
queryset = MembershipType.objects.none()
|
||||
permission = 'can_change_organizer_settings'
|
||||
permission = 'organizer.settings.general:write'
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.organizer.membership_types.all()
|
||||
@@ -714,7 +718,8 @@ with scopes_disabled():
|
||||
class MembershipViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = MembershipSerializer
|
||||
queryset = Membership.objects.none()
|
||||
permission = 'can_manage_customers'
|
||||
permission = 'organizer.customers:read'
|
||||
write_permission = 'organizer.customers:write'
|
||||
filter_backends = (DjangoFilterBackend,)
|
||||
filterset_class = MembershipFilter
|
||||
|
||||
@@ -764,8 +769,8 @@ with scopes_disabled():
|
||||
class SalesChannelViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = SalesChannelSerializer
|
||||
queryset = SalesChannel.objects.none()
|
||||
permission = 'can_change_organizer_settings'
|
||||
write_permission = 'can_change_organizer_settings'
|
||||
permission = 'organizer.settings.general:write'
|
||||
write_permission = 'organizer.settings.general:write'
|
||||
filter_backends = (DjangoFilterBackend,)
|
||||
filterset_class = SalesChannelFilter
|
||||
lookup_field = 'identifier'
|
||||
|
||||
@@ -204,7 +204,7 @@ class ShreddersMixin:
|
||||
|
||||
|
||||
class EventShreddersViewSet(ShreddersMixin, viewsets.ViewSet):
|
||||
permission = 'can_change_orders'
|
||||
permission = 'event.orders:write'
|
||||
|
||||
def get_serializer_kwargs(self):
|
||||
return {}
|
||||
|
||||
@@ -62,8 +62,8 @@ class VoucherViewSet(viewsets.ModelViewSet):
|
||||
ordering = ('id',)
|
||||
ordering_fields = ('id', 'code', 'max_usages', 'valid_until', 'value')
|
||||
filterset_class = VoucherFilter
|
||||
permission = 'can_view_vouchers'
|
||||
write_permission = 'can_change_vouchers'
|
||||
permission = 'event.vouchers:read'
|
||||
write_permission = 'event.vouchers:write'
|
||||
|
||||
@scopes_disabled() # we have an event check here, and we can save some performance on subqueries
|
||||
def get_queryset(self):
|
||||
|
||||
@@ -51,8 +51,8 @@ class WaitingListViewSet(viewsets.ModelViewSet):
|
||||
ordering = ('created', 'pk',)
|
||||
ordering_fields = ('id', 'created', 'email', 'item')
|
||||
filterset_class = WaitingListFilter
|
||||
permission = 'can_view_orders'
|
||||
write_permission = 'can_change_orders'
|
||||
permission = 'event.orders:read'
|
||||
write_permission = 'event.orders:write'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.waitinglistentries.all()
|
||||
|
||||
@@ -35,8 +35,8 @@ class WebhookFilter(FilterSet):
|
||||
class WebHookViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = WebHookSerializer
|
||||
queryset = WebHook.objects.none()
|
||||
permission = 'can_change_organizer_settings'
|
||||
write_permission = 'can_change_organizer_settings'
|
||||
permission = 'organizer.settings.general:write'
|
||||
write_permission = 'organizer.settings.general:write'
|
||||
filter_backends = (DjangoFilterBackend,)
|
||||
filterset_class = WebhookFilter
|
||||
|
||||
|
||||
@@ -224,7 +224,7 @@ class HistoryPasswordValidator:
|
||||
).delete()
|
||||
|
||||
|
||||
def has_event_access_permission(request, permission='can_change_event_settings'):
|
||||
def has_event_access_permission(request, permission='event.settings.general:write'):
|
||||
return (
|
||||
request.user.is_authenticated and
|
||||
request.user.has_event_permission(request.organizer, request.event, permission, request=request)
|
||||
|
||||
@@ -90,6 +90,7 @@ StaticMapping = namedtuple('StaticMapping', ('id', 'pretix_model', 'external_obj
|
||||
|
||||
class OutboundSyncProvider:
|
||||
max_attempts = 5
|
||||
list_field_joiner = "," # set to None to keep native lists in properties
|
||||
|
||||
def __init__(self, event):
|
||||
self.event = event
|
||||
@@ -281,7 +282,8 @@ class OutboundSyncProvider:
|
||||
'Please update value mapping for field "{field_name}" - option "{val}" not assigned'
|
||||
).format(field_name=key, val=val)])
|
||||
|
||||
val = ",".join(val)
|
||||
if self.list_field_joiner:
|
||||
val = self.list_field_joiner.join(val)
|
||||
return val
|
||||
|
||||
def get_properties(self, inputs: dict, property_mappings: List[dict]):
|
||||
|
||||
@@ -71,15 +71,20 @@ def assign_properties(
|
||||
return out
|
||||
|
||||
|
||||
def _add_to_list(out, field_name, current_value, new_item, list_sep):
|
||||
new_item = str(new_item)
|
||||
def _add_to_list(out, field_name, current_value, new_item_input, list_sep):
|
||||
if list_sep is not None:
|
||||
new_item = new_item.replace(list_sep, "")
|
||||
new_items = str(new_item_input).split(list_sep)
|
||||
current_value = current_value.split(list_sep) if current_value else []
|
||||
elif not isinstance(current_value, (list, tuple)):
|
||||
current_value = [str(current_value)]
|
||||
if new_item not in current_value:
|
||||
new_list = current_value + [new_item]
|
||||
else:
|
||||
new_items = [str(new_item_input)]
|
||||
if not isinstance(current_value, (list, tuple)):
|
||||
current_value = [str(current_value)]
|
||||
|
||||
new_list = list(current_value)
|
||||
for new_item in new_items:
|
||||
if new_item not in current_value:
|
||||
new_list.append(new_item)
|
||||
if new_list != current_value:
|
||||
if list_sep is not None:
|
||||
new_list = list_sep.join(new_list)
|
||||
out[field_name] = new_list
|
||||
|
||||
@@ -184,7 +184,7 @@ class OrganizerLevelExportMixin:
|
||||
The permission level required to use this exporter. Only useful for organizer-level exports,
|
||||
not for event-level exports.
|
||||
"""
|
||||
return 'can_view_orders'
|
||||
return 'event.orders:read'
|
||||
|
||||
|
||||
class ListExporter(BaseExporter):
|
||||
|
||||
@@ -47,7 +47,7 @@ from ..signals import register_multievent_data_exporters
|
||||
class CustomerListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
identifier = 'customerlist'
|
||||
verbose_name = gettext_lazy('Customer accounts')
|
||||
organizer_required_permission = 'can_manage_customers'
|
||||
organizer_required_permission = 'organizer.customers:write'
|
||||
category = pgettext_lazy('export_category', 'Customer accounts')
|
||||
description = gettext_lazy('Download a spreadsheet of all currently registered customer accounts.')
|
||||
|
||||
|
||||
@@ -1200,7 +1200,7 @@ class QuotaListExporter(ListExporter):
|
||||
class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
identifier = 'giftcardtransactionlist'
|
||||
verbose_name = gettext_lazy('Gift card transactions')
|
||||
organizer_required_permission = 'can_manage_gift_cards'
|
||||
organizer_required_permission = 'organizer.giftcards:write'
|
||||
category = pgettext_lazy('export_category', 'Gift cards')
|
||||
description = gettext_lazy('Download a spreadsheet of all gift card transactions.')
|
||||
repeatable_read = False
|
||||
@@ -1307,7 +1307,7 @@ class GiftcardRedemptionListExporter(ListExporter):
|
||||
class GiftcardListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
identifier = 'giftcardlist'
|
||||
verbose_name = gettext_lazy('Gift cards')
|
||||
organizer_required_permission = 'can_manage_gift_cards'
|
||||
organizer_required_permission = 'organizer.giftcards:write'
|
||||
category = pgettext_lazy('export_category', 'Gift cards')
|
||||
description = gettext_lazy('Download a spreadsheet of all gift cards including their current value.')
|
||||
|
||||
|
||||
@@ -66,8 +66,10 @@ from geoip2.errors import AddressNotFoundError
|
||||
from phonenumber_field.formfields import PhoneNumberField
|
||||
from phonenumber_field.phonenumber import PhoneNumber
|
||||
from phonenumber_field.widgets import PhoneNumberPrefixWidget
|
||||
from phonenumbers import NumberParseException, national_significant_number
|
||||
from phonenumbers.data import _COUNTRY_CODE_TO_REGION_CODE
|
||||
from phonenumbers import (
|
||||
COUNTRY_CODE_TO_REGION_CODE, REGION_CODE_FOR_NON_GEO_ENTITY,
|
||||
NumberParseException, national_significant_number,
|
||||
)
|
||||
from PIL import ImageOps
|
||||
|
||||
from pretix.base.forms.widgets import (
|
||||
@@ -305,7 +307,9 @@ class WrappedPhonePrefixSelect(Select):
|
||||
choices = [("", "---------")]
|
||||
|
||||
if initial:
|
||||
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
|
||||
for prefix, values in COUNTRY_CODE_TO_REGION_CODE.items():
|
||||
if all(v == REGION_CODE_FOR_NON_GEO_ENTITY for v in values):
|
||||
continue
|
||||
if initial in values:
|
||||
self.initial = "+%d" % prefix
|
||||
break
|
||||
@@ -437,7 +441,9 @@ def guess_phone_prefix_from_request(request, event):
|
||||
|
||||
|
||||
def get_phone_prefix(country):
|
||||
for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items():
|
||||
if country == REGION_CODE_FOR_NON_GEO_ENTITY:
|
||||
return None
|
||||
for prefix, values in COUNTRY_CODE_TO_REGION_CODE.items():
|
||||
if country in values:
|
||||
return prefix
|
||||
return None
|
||||
|
||||
@@ -46,7 +46,6 @@ from reportlab.lib.styles import ParagraphStyle, StyleSheet1
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.pdfmetrics import stringWidth
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
from reportlab.platypus import (
|
||||
BaseDocTemplate, Flowable, Frame, KeepTogether, NextPageTemplate,
|
||||
@@ -59,7 +58,8 @@ from pretix.base.services.currencies import SOURCE_NAMES
|
||||
from pretix.base.signals import register_invoice_renderers
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.helpers.reportlab import (
|
||||
FontFallbackParagraph, ThumbnailingImageReader, reshaper,
|
||||
FontFallbackParagraph, ThumbnailingImageReader, register_ttf_font_if_new,
|
||||
reshaper,
|
||||
)
|
||||
from pretix.presale.style import get_fonts
|
||||
|
||||
@@ -234,25 +234,25 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
||||
"""
|
||||
Register fonts with reportlab. By default, this registers the OpenSans font family
|
||||
"""
|
||||
pdfmetrics.registerFont(TTFont('OpenSans', finders.find('fonts/OpenSans-Regular.ttf')))
|
||||
pdfmetrics.registerFont(TTFont('OpenSansIt', finders.find('fonts/OpenSans-Italic.ttf')))
|
||||
pdfmetrics.registerFont(TTFont('OpenSansBd', finders.find('fonts/OpenSans-Bold.ttf')))
|
||||
pdfmetrics.registerFont(TTFont('OpenSansBI', finders.find('fonts/OpenSans-BoldItalic.ttf')))
|
||||
register_ttf_font_if_new('OpenSans', finders.find('fonts/OpenSans-Regular.ttf'))
|
||||
register_ttf_font_if_new('OpenSansIt', finders.find('fonts/OpenSans-Italic.ttf'))
|
||||
register_ttf_font_if_new('OpenSansBd', finders.find('fonts/OpenSans-Bold.ttf'))
|
||||
register_ttf_font_if_new('OpenSansBI', finders.find('fonts/OpenSans-BoldItalic.ttf'))
|
||||
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
|
||||
italic='OpenSansIt', boldItalic='OpenSansBI')
|
||||
|
||||
for family, styles in get_fonts(event=self.event, pdf_support_required=True).items():
|
||||
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
|
||||
register_ttf_font_if_new(family, finders.find(styles['regular']['truetype']))
|
||||
if family == self.event.settings.invoice_renderer_font:
|
||||
self.font_regular = family
|
||||
if 'bold' in styles:
|
||||
self.font_bold = family + ' B'
|
||||
if 'italic' in styles:
|
||||
pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype'])))
|
||||
register_ttf_font_if_new(family + ' I', finders.find(styles['italic']['truetype']))
|
||||
if 'bold' in styles:
|
||||
pdfmetrics.registerFont(TTFont(family + ' B', finders.find(styles['bold']['truetype'])))
|
||||
register_ttf_font_if_new(family + ' B', finders.find(styles['bold']['truetype']))
|
||||
if 'bolditalic' in styles:
|
||||
pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype'])))
|
||||
register_ttf_font_if_new(family + ' B I', finders.find(styles['bolditalic']['truetype']))
|
||||
|
||||
def _normalize(self, text):
|
||||
# reportlab does not support unicode combination characters
|
||||
|
||||
@@ -73,6 +73,9 @@ class PeppolIdValidator:
|
||||
"0205": "[A-Z0-9]+",
|
||||
"0221": "T[0-9]{13}",
|
||||
"0230": ".*",
|
||||
"0244": "[0-9]{13}",
|
||||
"0245": "[0-9]{10}",
|
||||
"0246": "DE[0-9]{9}(-[0-9]{5})?(\\.[0-9A-Z]{1,8})?",
|
||||
"9901": ".*",
|
||||
"9902": "[1-9][0-9]{7}",
|
||||
"9904": "DK[0-9]{8}",
|
||||
@@ -120,7 +123,6 @@ class PeppolIdValidator:
|
||||
"9951": ".*",
|
||||
"9952": ".*",
|
||||
"9953": ".*",
|
||||
"9954": ".*",
|
||||
"9956": "0[0-9]{9}",
|
||||
"9957": ".*",
|
||||
"9959": ".*",
|
||||
|
||||
129
src/pretix/base/migrations/0297_pluggable_permissions.py
Normal file
129
src/pretix/base/migrations/0297_pluggable_permissions.py
Normal file
@@ -0,0 +1,129 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
from pretix.helpers.permission_migration import (
|
||||
OLD_TO_NEW_EVENT_MIGRATION, OLD_TO_NEW_ORGANIZER_MIGRATION,
|
||||
)
|
||||
|
||||
|
||||
def migrate_teams_forward(apps, schema_editor):
|
||||
Team = apps.get_model("pretixbase", "Team")
|
||||
|
||||
for team in Team.objects.iterator():
|
||||
if all(getattr(team, k) for k in OLD_TO_NEW_EVENT_MIGRATION.keys() if k != "can_checkin_orders"):
|
||||
team.all_event_permissions = True
|
||||
team.limit_event_permissions = {}
|
||||
else:
|
||||
team.all_event_permissions = False
|
||||
for k, v in OLD_TO_NEW_EVENT_MIGRATION.items():
|
||||
if getattr(team, k):
|
||||
team.limit_event_permissions.update({kk: True for kk in v})
|
||||
|
||||
if all(getattr(team, k) for k in OLD_TO_NEW_ORGANIZER_MIGRATION.keys()):
|
||||
team.all_organizer_permissions = True
|
||||
team.limit_organizer_permissions = {}
|
||||
else:
|
||||
team.all_organizer_permissions = False
|
||||
for k, v in OLD_TO_NEW_ORGANIZER_MIGRATION.items():
|
||||
if getattr(team, k):
|
||||
team.limit_organizer_permissions.update({kk: True for kk in v})
|
||||
|
||||
team.save(update_fields=[
|
||||
"all_event_permissions", "limit_event_permissions", "all_organizer_permissions", "limit_organizer_permissions"
|
||||
])
|
||||
|
||||
|
||||
def migrate_teams_backward(apps, schema_editor):
|
||||
Team = apps.get_model("pretixbase", "Team")
|
||||
|
||||
for team in Team.objects.iterator():
|
||||
for k, v in OLD_TO_NEW_EVENT_MIGRATION.items():
|
||||
setattr(team, k, team.all_event_permissions or all(team.limit_event_permissions.get(kk) for kk in v))
|
||||
for k, v in OLD_TO_NEW_ORGANIZER_MIGRATION.items():
|
||||
setattr(team, k, team.all_organizer_permissions or all(team.limit_organizer_permissions.get(kk) for kk in v))
|
||||
team.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0296_invoice_invoice_from_state"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="team",
|
||||
name="all_event_permissions",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="team",
|
||||
name="all_organizer_permissions",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="team",
|
||||
name="limit_event_permissions",
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="team",
|
||||
name="limit_organizer_permissions",
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.RunPython(
|
||||
migrate_teams_forward,
|
||||
migrate_teams_backward,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="team",
|
||||
name="can_change_event_settings",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="team",
|
||||
name="can_change_items",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="team",
|
||||
name="can_change_orders",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="team",
|
||||
name="can_change_organizer_settings",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="team",
|
||||
name="can_change_teams",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="team",
|
||||
name="can_change_vouchers",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="team",
|
||||
name="can_checkin_orders",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="team",
|
||||
name="can_create_events",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="team",
|
||||
name="can_manage_customers",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="team",
|
||||
name="can_manage_gift_cards",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="team",
|
||||
name="can_manage_reusable_media",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="team",
|
||||
name="can_view_orders",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="team",
|
||||
name="can_view_vouchers",
|
||||
),
|
||||
]
|
||||
@@ -472,7 +472,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
:return: set
|
||||
"""
|
||||
teams = self._get_teams_for_event(organizer, event)
|
||||
sets = [t.permission_set() for t in teams]
|
||||
sets = [t.event_permission_set() for t in teams]
|
||||
if sets:
|
||||
return set.union(*sets)
|
||||
else:
|
||||
@@ -486,7 +486,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
:return: set
|
||||
"""
|
||||
teams = self._get_teams_for_organizer(organizer)
|
||||
sets = [t.permission_set() for t in teams]
|
||||
sets = [t.organizer_permission_set() for t in teams]
|
||||
if sets:
|
||||
return set.union(*sets)
|
||||
else:
|
||||
@@ -501,7 +501,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
|
||||
:param organizer: The organizer of the event
|
||||
:param event: The event to check
|
||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||
:param perm_name: The permission, e.g. ``event.orders:read``
|
||||
:param request: The current request (optional)
|
||||
:param session_key: The current session key (optional)
|
||||
:return: bool
|
||||
@@ -513,8 +513,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
if teams:
|
||||
self._teamcache['e{}'.format(event.pk)] = teams
|
||||
if isinstance(perm_name, (tuple, list)):
|
||||
return any([any(team.has_permission(p) for team in teams) for p in perm_name])
|
||||
if not perm_name or any([team.has_permission(perm_name) for team in teams]):
|
||||
return any([any(team.has_event_permission(p) for team in teams) for p in perm_name])
|
||||
if not perm_name or any([team.has_event_permission(perm_name) for team in teams]):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -524,7 +524,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
to the organizer ``organizer``.
|
||||
|
||||
:param organizer: The organizer to check
|
||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||
:param perm_name: The permission, e.g. ``organizer.events:create``
|
||||
:param request: The current request (optional). Required to detect staff sessions properly.
|
||||
:return: bool
|
||||
"""
|
||||
@@ -533,8 +533,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
teams = self._get_teams_for_organizer(organizer)
|
||||
if teams:
|
||||
if isinstance(perm_name, (tuple, list)):
|
||||
return any([any(team.has_permission(p) for team in teams) for p in perm_name])
|
||||
if not perm_name or any([team.has_permission(perm_name) for team in teams]):
|
||||
return any([any(team.has_organizer_permission(p) for team in teams) for p in perm_name])
|
||||
if not perm_name or any([team.has_organizer_permission(perm_name) for team in teams]):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -565,14 +565,15 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
:return: Iterable of Events
|
||||
"""
|
||||
from .event import Event
|
||||
from .organizer import TeamQuerySet
|
||||
|
||||
if request and self.has_active_staff_session(request.session.session_key):
|
||||
return Event.objects.all()
|
||||
|
||||
if isinstance(permission, (tuple, list)):
|
||||
q = reduce(operator.or_, [Q(**{p: True}) for p in permission])
|
||||
q = reduce(operator.or_, [TeamQuerySet.event_permission_q(p) for p in permission])
|
||||
else:
|
||||
q = Q(**{permission: True})
|
||||
q = TeamQuerySet.event_permission_q(permission)
|
||||
|
||||
return Event.objects.filter(
|
||||
Q(organizer_id__in=self.teams.filter(q, all_events=True).values_list('organizer', flat=True))
|
||||
@@ -605,14 +606,13 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
:return: Iterable of Organizers
|
||||
"""
|
||||
from .event import Organizer
|
||||
from .organizer import TeamQuerySet
|
||||
|
||||
if request and self.has_active_staff_session(request.session.session_key):
|
||||
return Organizer.objects.all()
|
||||
|
||||
kwargs = {permission: True}
|
||||
|
||||
return Organizer.objects.filter(
|
||||
id__in=self.teams.filter(**kwargs).values_list('organizer', flat=True)
|
||||
id__in=self.teams.filter(TeamQuerySet.organizer_permission_q(permission)).values_list('organizer', flat=True)
|
||||
)
|
||||
|
||||
def has_active_staff_session(self, session_key=None):
|
||||
|
||||
@@ -59,6 +59,37 @@ class CachedFile(models.Model):
|
||||
web_download = models.BooleanField(default=True) # allow web download, True for backwards compatibility in plugins
|
||||
session_key = models.TextField(null=True, blank=True) # only allow download in this session
|
||||
|
||||
def session_key_for_request(self, request, salt=None):
|
||||
from ...api.models import OAuthAccessToken, OAuthApplication
|
||||
from .devices import Device
|
||||
from .organizer import TeamAPIToken
|
||||
|
||||
if hasattr(request, "auth") and isinstance(request.auth, OAuthAccessToken):
|
||||
k = f'app:{request.auth.application.pk}'
|
||||
elif hasattr(request, "auth") and isinstance(request.auth, OAuthApplication):
|
||||
k = f'app:{request.auth.pk}'
|
||||
elif hasattr(request, "auth") and isinstance(request.auth, TeamAPIToken):
|
||||
k = f'token:{request.auth.pk}'
|
||||
elif hasattr(request, "auth") and isinstance(request.auth, Device):
|
||||
k = f'device:{request.auth.pk}'
|
||||
elif request.session.session_key:
|
||||
k = request.session.session_key
|
||||
else:
|
||||
raise ValueError("No auth method found to bind to")
|
||||
|
||||
if salt:
|
||||
k = f"{k}!{salt}"
|
||||
return k
|
||||
|
||||
def allowed_for_session(self, request, salt=None):
|
||||
return (
|
||||
not self.session_key or
|
||||
self.session_key_for_request(request, salt) == self.session_key
|
||||
)
|
||||
|
||||
def bind_to_session(self, request, salt=None):
|
||||
self.session_key = self.session_key_for_request(request, salt)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=CachedFile)
|
||||
def cached_file_delete(sender, instance, **kwargs):
|
||||
|
||||
@@ -189,13 +189,19 @@ class Device(LoggedModel):
|
||||
kwargs['update_fields'] = {'device_id'}.union(kwargs['update_fields'])
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def permission_set(self) -> set:
|
||||
def _event_permission_set(self) -> set:
|
||||
return {
|
||||
'can_view_orders',
|
||||
'can_change_orders',
|
||||
'can_view_vouchers',
|
||||
'can_manage_gift_cards',
|
||||
'can_manage_reusable_media',
|
||||
'event.orders:read',
|
||||
'event.orders:write',
|
||||
'event.vouchers:read',
|
||||
}
|
||||
|
||||
def _organizer_permission_set(self) -> set:
|
||||
return {
|
||||
'organizer.giftcards:read',
|
||||
'organizer.giftcards:write',
|
||||
'organizer.reusablemedia:read',
|
||||
'organizer.reusablemedia:write',
|
||||
}
|
||||
|
||||
def get_event_permission_set(self, organizer, event) -> set:
|
||||
@@ -209,7 +215,7 @@ class Device(LoggedModel):
|
||||
has_event_access = (self.all_events and organizer == self.organizer) or (
|
||||
event in self.limit_events.all()
|
||||
)
|
||||
return self.permission_set() if has_event_access else set()
|
||||
return self._event_permission_set() if has_event_access else set()
|
||||
|
||||
def get_organizer_permission_set(self, organizer) -> set:
|
||||
"""
|
||||
@@ -218,7 +224,7 @@ class Device(LoggedModel):
|
||||
:param organizer: The organizer of the event
|
||||
:return: set of permissions
|
||||
"""
|
||||
return self.permission_set() if self.organizer == organizer else set()
|
||||
return self._organizer_permission_set() if self.organizer == organizer else set()
|
||||
|
||||
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
|
||||
"""
|
||||
@@ -227,7 +233,7 @@ class Device(LoggedModel):
|
||||
|
||||
:param organizer: The organizer of the event
|
||||
:param event: The event to check
|
||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||
:param perm_name: The permission, e.g. ``event.orders:read``
|
||||
:param request: This parameter is ignored and only defined for compatibility reasons.
|
||||
:return: bool
|
||||
"""
|
||||
@@ -235,8 +241,8 @@ class Device(LoggedModel):
|
||||
event in self.limit_events.all()
|
||||
)
|
||||
if isinstance(perm_name, (tuple, list)):
|
||||
return has_event_access and any(p in self.permission_set() for p in perm_name)
|
||||
return has_event_access and (not perm_name or perm_name in self.permission_set())
|
||||
return has_event_access and any(p in self._event_permission_set() for p in perm_name)
|
||||
return has_event_access and (not perm_name or perm_name in self._event_permission_set())
|
||||
|
||||
def has_organizer_permission(self, organizer, perm_name=None, request=None):
|
||||
"""
|
||||
@@ -244,13 +250,13 @@ class Device(LoggedModel):
|
||||
to the organizer ``organizer``.
|
||||
|
||||
:param organizer: The organizer to check
|
||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||
:param perm_name: The permission, e.g. ``organizer.events:create``
|
||||
:param request: This parameter is ignored and only defined for compatibility reasons.
|
||||
:return: bool
|
||||
"""
|
||||
if isinstance(perm_name, (tuple, list)):
|
||||
return organizer == self.organizer and any(p in self.permission_set() for p in perm_name)
|
||||
return organizer == self.organizer and (not perm_name or perm_name in self.permission_set())
|
||||
return organizer == self.organizer and any(p in self._organizer_permission_set() for p in perm_name)
|
||||
return organizer == self.organizer and (not perm_name or perm_name in self._organizer_permission_set())
|
||||
|
||||
def get_events_with_any_permission(self):
|
||||
"""
|
||||
@@ -271,8 +277,8 @@ class Device(LoggedModel):
|
||||
:return: Iterable of Events
|
||||
"""
|
||||
if (
|
||||
isinstance(permission, (list, tuple)) and any(p in self.permission_set() for p in permission)
|
||||
) or (isinstance(permission, str) and permission in self.permission_set()):
|
||||
isinstance(permission, (list, tuple)) and any(p in self._event_permission_set() for p in permission)
|
||||
) or (isinstance(permission, str) and permission in self._event_permission_set()):
|
||||
return self.get_events_with_any_permission()
|
||||
else:
|
||||
return self.organizer.events.none()
|
||||
|
||||
@@ -1386,14 +1386,13 @@ class Event(EventMixin, LoggedModel):
|
||||
from .auth import User
|
||||
|
||||
if permission:
|
||||
kwargs = {permission: True}
|
||||
qs = Team.objects.with_event_permission(permission)
|
||||
else:
|
||||
kwargs = {}
|
||||
qs = Team.objects.all()
|
||||
|
||||
team_with_perm = Team.objects.filter(
|
||||
team_with_perm = qs.filter(
|
||||
members__pk=OuterRef('pk'),
|
||||
organizer=self.organizer,
|
||||
**kwargs
|
||||
).filter(
|
||||
Q(all_events=True) | Q(limit_events__pk=self.pk)
|
||||
)
|
||||
|
||||
@@ -31,9 +31,10 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import operator
|
||||
import string
|
||||
from datetime import date, datetime, time
|
||||
from functools import reduce
|
||||
|
||||
import pytz_deprecation_shim
|
||||
from django.conf import settings
|
||||
@@ -53,6 +54,10 @@ from i18nfield.strings import LazyI18nString
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.validators import OrganizerSlugBanlistValidator
|
||||
|
||||
from ...helpers.permission_migration import (
|
||||
OLD_TO_NEW_EVENT_COMPAT, OLD_TO_NEW_ORGANIZER_COMPAT,
|
||||
LegacyPermissionProperty,
|
||||
)
|
||||
from ..settings import settings_hierarkey
|
||||
from .auth import User
|
||||
|
||||
@@ -309,6 +314,32 @@ def generate_api_token():
|
||||
return get_random_string(length=64, allowed_chars=string.ascii_lowercase + string.digits)
|
||||
|
||||
|
||||
class TeamQuerySet(models.QuerySet):
|
||||
@classmethod
|
||||
def event_permission_q(cls, perm_name):
|
||||
if perm_name.startswith('can_') and perm_name in OLD_TO_NEW_EVENT_COMPAT: # legacy
|
||||
return reduce(operator.and_, [cls.event_permission_q(p) for p in OLD_TO_NEW_EVENT_COMPAT[perm_name]])
|
||||
return (
|
||||
Q(all_event_permissions=True) |
|
||||
Q(**{f'limit_event_permissions__{perm_name}': True})
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def organizer_permission_q(cls, perm_name):
|
||||
if perm_name.startswith('can_') and perm_name in OLD_TO_NEW_ORGANIZER_COMPAT: # legacy
|
||||
return reduce(operator.and_, [cls.organizer_permission_q(p) for p in OLD_TO_NEW_ORGANIZER_COMPAT[perm_name]])
|
||||
return (
|
||||
Q(all_organizer_permissions=True) |
|
||||
Q(**{f'limit_organizer_permissions__{perm_name}': True})
|
||||
)
|
||||
|
||||
def with_event_permission(self, perm_name):
|
||||
return self.filter(self.event_permission_q(perm_name))
|
||||
|
||||
def with_organizer_permission(self, perm_name):
|
||||
return self.filter(self.organizer_permission_q(perm_name))
|
||||
|
||||
|
||||
class Team(LoggedModel):
|
||||
"""
|
||||
A team is a collection of people given certain access rights to one or more events of an organizer.
|
||||
@@ -321,36 +352,10 @@ class Team(LoggedModel):
|
||||
:param all_events: Whether this team has access to all events of this organizer
|
||||
:type all_events: bool
|
||||
:param limit_events: A set of events this team has access to. Irrelevant if ``all_events`` is ``True``.
|
||||
:param can_create_events: Whether or not the members can create new events with this organizer account.
|
||||
:type can_create_events: bool
|
||||
:param can_change_teams: If ``True``, the members can change the teams of this organizer account.
|
||||
:type can_change_teams: bool
|
||||
:param can_manage_customers: If ``True``, the members can view and change organizer-level customer accounts.
|
||||
:type can_manage_customers: bool
|
||||
:param can_manage_reusable_media: If ``True``, the members can view and change organizer-level reusable media.
|
||||
:type can_manage_reusable_media: bool
|
||||
:param can_change_organizer_settings: If ``True``, the members can change the settings of this organizer account.
|
||||
:type can_change_organizer_settings: bool
|
||||
:param can_change_event_settings: If ``True``, the members can change the settings of the associated events.
|
||||
:type can_change_event_settings: bool
|
||||
:param can_change_items: If ``True``, the members can change and add items and related objects for the associated events.
|
||||
:type can_change_items: bool
|
||||
:param can_view_orders: If ``True``, the members can inspect details of all orders of the associated events.
|
||||
:type can_view_orders: bool
|
||||
:param can_change_orders: If ``True``, the members can change details of orders of the associated events.
|
||||
:type can_change_orders: bool
|
||||
:param can_checkin_orders: If ``True``, the members can perform check-in related actions.
|
||||
:type can_checkin_orders: bool
|
||||
:param can_view_vouchers: If ``True``, the members can inspect details of all vouchers of the associated events.
|
||||
:type can_view_vouchers: bool
|
||||
:param can_change_vouchers: If ``True``, the members can change and create vouchers for the associated events.
|
||||
:type can_change_vouchers: bool
|
||||
"""
|
||||
organizer = models.ForeignKey(Organizer, related_name="teams", on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=190, verbose_name=_("Team name"))
|
||||
members = models.ManyToManyField(User, related_name="teams", verbose_name=_("Team members"))
|
||||
all_events = models.BooleanField(default=False, verbose_name=_("All events (including newly created ones)"))
|
||||
limit_events = models.ManyToManyField('Event', verbose_name=_("Limit to events"), blank=True)
|
||||
require_2fa = models.BooleanField(
|
||||
default=False, verbose_name=_("Require all members of this team to use two-factor authentication"),
|
||||
help_text=_("If you turn this on, all members of the team will be required to either set up two-factor "
|
||||
@@ -358,62 +363,33 @@ class Team(LoggedModel):
|
||||
"all users.")
|
||||
)
|
||||
|
||||
can_create_events = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can create events"),
|
||||
)
|
||||
can_change_teams = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change teams and permissions"),
|
||||
)
|
||||
can_change_organizer_settings = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change organizer settings"),
|
||||
help_text=_('Someone with this setting can get access to most data of all of your events, i.e. via privacy '
|
||||
'reports, so be careful who you add to this team!')
|
||||
)
|
||||
can_manage_customers = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can manage customer accounts")
|
||||
)
|
||||
can_manage_reusable_media = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can manage reusable media")
|
||||
)
|
||||
can_manage_gift_cards = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can manage gift cards")
|
||||
)
|
||||
can_change_event_settings = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change event settings")
|
||||
)
|
||||
can_change_items = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change product settings")
|
||||
)
|
||||
can_view_orders = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can view orders")
|
||||
)
|
||||
can_change_orders = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change orders")
|
||||
)
|
||||
can_checkin_orders = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can perform check-ins"),
|
||||
help_text=_('This includes searching for attendees, which can be used to obtain personal information about '
|
||||
'attendees. Users with "can change orders" can also perform check-ins.')
|
||||
)
|
||||
can_view_vouchers = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can view vouchers")
|
||||
)
|
||||
can_change_vouchers = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change vouchers")
|
||||
)
|
||||
# Scope
|
||||
all_events = models.BooleanField(default=False, verbose_name=_("All events (including newly created ones)"))
|
||||
limit_events = models.ManyToManyField('Event', verbose_name=_("Limit to events"), blank=True)
|
||||
|
||||
# Permissions
|
||||
# We store them as {key: True} instead of [key] because otherwise not all lookups we need are supported on SQLite
|
||||
all_event_permissions = models.BooleanField(default=False, verbose_name=_("All event permissions"))
|
||||
limit_event_permissions = models.JSONField(default=dict, verbose_name=_("Event permissions"))
|
||||
all_organizer_permissions = models.BooleanField(default=False, verbose_name=_("All organizer permissions"))
|
||||
limit_organizer_permissions = models.JSONField(default=dict, verbose_name=_("Organizer permissions"))
|
||||
|
||||
# Legacy lookups for plugin compatibility
|
||||
can_change_event_settings = LegacyPermissionProperty()
|
||||
can_change_items = LegacyPermissionProperty()
|
||||
can_view_orders = LegacyPermissionProperty()
|
||||
can_change_orders = LegacyPermissionProperty()
|
||||
can_checkin_orders = LegacyPermissionProperty()
|
||||
can_view_vouchers = LegacyPermissionProperty()
|
||||
can_change_vouchers = LegacyPermissionProperty()
|
||||
can_create_events = LegacyPermissionProperty()
|
||||
can_change_organizer_settings = LegacyPermissionProperty()
|
||||
can_change_teams = LegacyPermissionProperty()
|
||||
can_manage_gift_cards = LegacyPermissionProperty()
|
||||
can_manage_customers = LegacyPermissionProperty()
|
||||
can_manage_reusable_media = LegacyPermissionProperty()
|
||||
|
||||
objects = TeamQuerySet.as_manager()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return _("%(name)s on %(object)s") % {
|
||||
@@ -421,21 +397,51 @@ class Team(LoggedModel):
|
||||
'object': str(self.organizer),
|
||||
}
|
||||
|
||||
def permission_set(self) -> set:
|
||||
attribs = dir(self)
|
||||
return {
|
||||
a for a in attribs if a.startswith('can_') and self.has_permission(a)
|
||||
}
|
||||
def event_permission_set(self, include_legacy=True) -> set:
|
||||
from ..permissions import get_all_event_permissions
|
||||
|
||||
result = set()
|
||||
for permission in get_all_event_permissions().keys():
|
||||
if self.all_event_permissions or self.limit_event_permissions.get(permission):
|
||||
result.add(permission)
|
||||
|
||||
if include_legacy:
|
||||
# Add legacy permissions as well for plugin compatibility
|
||||
for k, v in OLD_TO_NEW_EVENT_COMPAT.items():
|
||||
if self.all_event_permissions or all(self.limit_event_permissions.get(kk) for kk in v):
|
||||
result.add(k)
|
||||
|
||||
return result
|
||||
|
||||
def organizer_permission_set(self, include_legacy=True) -> set:
|
||||
from ..permissions import get_all_organizer_permissions
|
||||
|
||||
result = set()
|
||||
for permission in get_all_organizer_permissions().keys():
|
||||
if self.all_organizer_permissions or self.limit_organizer_permissions.get(permission):
|
||||
result.add(permission)
|
||||
|
||||
if include_legacy:
|
||||
# Add legacy permissions as well for plugin compatibility
|
||||
for k, v in OLD_TO_NEW_ORGANIZER_COMPAT.items():
|
||||
if self.all_organizer_permissions or all(self.limit_organizer_permissions.get(kk) for kk in v):
|
||||
result.add(k)
|
||||
|
||||
return result
|
||||
|
||||
@property
|
||||
def can_change_settings(self): # Legacy compatiblilty
|
||||
def can_change_settings(self): # Legacy compatibility
|
||||
return self.can_change_event_settings
|
||||
|
||||
def has_permission(self, perm_name):
|
||||
try:
|
||||
def has_event_permission(self, perm_name):
|
||||
if perm_name.startswith('can_') and hasattr(self, perm_name): # legacy
|
||||
return getattr(self, perm_name)
|
||||
except AttributeError:
|
||||
raise ValueError('Invalid required permission: %s' % perm_name)
|
||||
return self.all_event_permissions or self.limit_event_permissions.get(perm_name, False)
|
||||
|
||||
def has_organizer_permission(self, perm_name):
|
||||
if perm_name.startswith('can_') and hasattr(self, perm_name): # legacy
|
||||
return getattr(self, perm_name)
|
||||
return self.all_organizer_permissions or self.limit_organizer_permissions.get(perm_name, False)
|
||||
|
||||
def permission_for_event(self, event):
|
||||
if self.all_events:
|
||||
@@ -447,6 +453,19 @@ class Team(LoggedModel):
|
||||
def active_tokens(self):
|
||||
return self.tokens.filter(active=True)
|
||||
|
||||
def save(self, **kwargs):
|
||||
if not isinstance(self.limit_event_permissions, dict):
|
||||
raise TypeError("Permissions must be a dictionary")
|
||||
if not isinstance(self.limit_organizer_permissions, dict):
|
||||
raise TypeError("Permissions must be a dictionary")
|
||||
for k in self.limit_event_permissions.values():
|
||||
if k is not True:
|
||||
raise TypeError("Permissions must only contain True values")
|
||||
for k in self.limit_organizer_permissions.values():
|
||||
if k is not True:
|
||||
raise TypeError("Permissions must only contain True values")
|
||||
return super().save(**kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Team")
|
||||
verbose_name_plural = _("Teams")
|
||||
@@ -503,7 +522,7 @@ class TeamAPIToken(models.Model):
|
||||
has_event_access = (self.team.all_events and organizer == self.team.organizer) or (
|
||||
event in self.team.limit_events.all()
|
||||
)
|
||||
return self.team.permission_set() if has_event_access else set()
|
||||
return self.team.event_permission_set() if has_event_access else set()
|
||||
|
||||
def get_organizer_permission_set(self, organizer) -> set:
|
||||
"""
|
||||
@@ -512,7 +531,7 @@ class TeamAPIToken(models.Model):
|
||||
:param organizer: The organizer of the event
|
||||
:return: set of permissions
|
||||
"""
|
||||
return self.team.permission_set() if self.team.organizer == organizer else set()
|
||||
return self.team.organizer_permission_set() if self.team.organizer == organizer else set()
|
||||
|
||||
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
|
||||
"""
|
||||
@@ -521,7 +540,7 @@ class TeamAPIToken(models.Model):
|
||||
|
||||
:param organizer: The organizer of the event
|
||||
:param event: The event to check
|
||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||
:param perm_name: The permission, e.g. ``event.orders:read``
|
||||
:param request: This parameter is ignored and only defined for compatibility reasons.
|
||||
:return: bool
|
||||
"""
|
||||
@@ -529,8 +548,8 @@ class TeamAPIToken(models.Model):
|
||||
event in self.team.limit_events.all()
|
||||
)
|
||||
if isinstance(perm_name, (tuple, list)):
|
||||
return has_event_access and any(self.team.has_permission(p) for p in perm_name)
|
||||
return has_event_access and (not perm_name or self.team.has_permission(perm_name))
|
||||
return has_event_access and any(self.team.has_event_permission(p) for p in perm_name)
|
||||
return has_event_access and (not perm_name or self.team.has_event_permission(perm_name))
|
||||
|
||||
def has_organizer_permission(self, organizer, perm_name=None, request=None):
|
||||
"""
|
||||
@@ -538,13 +557,13 @@ class TeamAPIToken(models.Model):
|
||||
to the organizer ``organizer``.
|
||||
|
||||
:param organizer: The organizer to check
|
||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||
:param perm_name: The permission, e.g. ``organizer:events.create``
|
||||
:param request: This parameter is ignored and only defined for compatibility reasons.
|
||||
:return: bool
|
||||
"""
|
||||
if isinstance(perm_name, (tuple, list)):
|
||||
return organizer == self.team.organizer and any(self.team.has_permission(p) for p in perm_name)
|
||||
return organizer == self.team.organizer and (not perm_name or self.team.has_permission(perm_name))
|
||||
return organizer == self.team.organizer and any(self.team.has_organizer_permission(p) for p in perm_name)
|
||||
return organizer == self.team.organizer and (not perm_name or self.team.has_organizer_permission(perm_name))
|
||||
|
||||
def get_events_with_any_permission(self):
|
||||
"""
|
||||
@@ -565,8 +584,8 @@ class TeamAPIToken(models.Model):
|
||||
:return: Iterable of Events
|
||||
"""
|
||||
if (
|
||||
isinstance(permission, (list, tuple)) and any(getattr(self.team, p, False) for p in permission)
|
||||
) or (isinstance(permission, str) and getattr(self.team, permission, False)):
|
||||
isinstance(permission, (list, tuple)) and any(self.team.has_event_permission(p) for p in permission)
|
||||
) or (isinstance(permission, str) and self.team.has_event_permission(permission)):
|
||||
return self.get_events_with_any_permission()
|
||||
else:
|
||||
return self.team.organizer.events.none()
|
||||
|
||||
@@ -151,7 +151,7 @@ def get_all_notification_types(event=None):
|
||||
|
||||
|
||||
class ParametrizedOrderNotificationType(NotificationType):
|
||||
required_permission = "can_view_orders"
|
||||
required_permission = "event.orders:read"
|
||||
|
||||
def __init__(self, event, action_type, verbose_name, title):
|
||||
self._action_type = action_type
|
||||
|
||||
@@ -71,9 +71,7 @@ from reportlab.lib.colors import Color
|
||||
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
|
||||
from reportlab.lib.styles import ParagraphStyle
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.pdfmetrics import getAscentDescent
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
from reportlab.platypus import Paragraph
|
||||
|
||||
@@ -84,7 +82,9 @@ from pretix.base.signals import layout_image_variables, layout_text_variables
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.base.templatetags.phone_format import phone_format
|
||||
from pretix.helpers.daterange import datetimerange
|
||||
from pretix.helpers.reportlab import ThumbnailingImageReader, reshaper
|
||||
from pretix.helpers.reportlab import (
|
||||
ThumbnailingImageReader, register_ttf_font_if_new, reshaper,
|
||||
)
|
||||
from pretix.presale.style import get_fonts
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -794,19 +794,19 @@ class Renderer:
|
||||
def _register_fonts(cls, event: Event = None):
|
||||
if hasattr(cls, '_fonts_registered'):
|
||||
return
|
||||
pdfmetrics.registerFont(TTFont('Open Sans', finders.find('fonts/OpenSans-Regular.ttf')))
|
||||
pdfmetrics.registerFont(TTFont('Open Sans I', finders.find('fonts/OpenSans-Italic.ttf')))
|
||||
pdfmetrics.registerFont(TTFont('Open Sans B', finders.find('fonts/OpenSans-Bold.ttf')))
|
||||
pdfmetrics.registerFont(TTFont('Open Sans B I', finders.find('fonts/OpenSans-BoldItalic.ttf')))
|
||||
register_ttf_font_if_new('Open Sans', finders.find('fonts/OpenSans-Regular.ttf'))
|
||||
register_ttf_font_if_new('Open Sans I', finders.find('fonts/OpenSans-Italic.ttf'))
|
||||
register_ttf_font_if_new('Open Sans B', finders.find('fonts/OpenSans-Bold.ttf'))
|
||||
register_ttf_font_if_new('Open Sans B I', finders.find('fonts/OpenSans-BoldItalic.ttf'))
|
||||
|
||||
for family, styles in get_fonts(event, pdf_support_required=True).items():
|
||||
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
|
||||
register_ttf_font_if_new(family, finders.find(styles['regular']['truetype']))
|
||||
if 'italic' in styles:
|
||||
pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype'])))
|
||||
register_ttf_font_if_new(family + ' I', finders.find(styles['italic']['truetype']))
|
||||
if 'bold' in styles:
|
||||
pdfmetrics.registerFont(TTFont(family + ' B', finders.find(styles['bold']['truetype'])))
|
||||
register_ttf_font_if_new(family + ' B', finders.find(styles['bold']['truetype']))
|
||||
if 'bolditalic' in styles:
|
||||
pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype'])))
|
||||
register_ttf_font_if_new(family + ' B I', finders.find(styles['bolditalic']['truetype']))
|
||||
|
||||
cls._fonts_registered = True
|
||||
|
||||
|
||||
117
src/pretix/base/permissions.py
Normal file
117
src/pretix/base/permissions.py
Normal file
@@ -0,0 +1,117 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import logging
|
||||
from collections import OrderedDict, namedtuple
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
|
||||
from pretix.base.signals import (
|
||||
register_event_permissions, register_organizer_permissions,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_ALL_EVENT_PERMISSIONS = None
|
||||
_ALL_ORGANIZER_PERMISSIONS = None
|
||||
|
||||
|
||||
Permission = namedtuple('Permission', ('name', 'label', 'plugin_name', 'help_text'))
|
||||
|
||||
|
||||
def get_all_event_permissions():
|
||||
global _ALL_EVENT_PERMISSIONS
|
||||
|
||||
if _ALL_EVENT_PERMISSIONS:
|
||||
return _ALL_EVENT_PERMISSIONS
|
||||
|
||||
types = OrderedDict()
|
||||
for recv, ret in register_event_permissions.send(None):
|
||||
if isinstance(ret, (list, tuple)):
|
||||
for r in ret:
|
||||
types[r.name] = r
|
||||
else:
|
||||
types[ret.name] = ret
|
||||
_ALL_EVENT_PERMISSIONS = types
|
||||
return types
|
||||
|
||||
|
||||
def get_all_organizer_permissions():
|
||||
global _ALL_ORGANIZER_PERMISSIONS
|
||||
|
||||
if _ALL_ORGANIZER_PERMISSIONS:
|
||||
return _ALL_ORGANIZER_PERMISSIONS
|
||||
|
||||
types = OrderedDict()
|
||||
for recv, ret in register_organizer_permissions.send(None):
|
||||
if isinstance(ret, (list, tuple)):
|
||||
for r in ret:
|
||||
types[r.name] = r
|
||||
else:
|
||||
types[ret.name] = ret
|
||||
_ALL_ORGANIZER_PERMISSIONS = types
|
||||
return types
|
||||
|
||||
|
||||
@receiver(register_event_permissions, dispatch_uid="base_register_default_event_permissions")
|
||||
def register_default_event_permissions(sender, **kwargs):
|
||||
return [
|
||||
Permission("event.settings.general:write", _("Change general settings"), None,
|
||||
_("This includes access to all settings not listed explicitly below, including plugin settings.")),
|
||||
Permission("event.settings.payment:write", _("Change payment settings"), None, None),
|
||||
Permission("event.settings.tax:write", _("Change tax rules"), None, None),
|
||||
Permission("event.settings.invoicing:write", _("Change invoicing settings"), None, None),
|
||||
Permission("event.subevents:write", pgettext_lazy("subevent", "Change event series dates"), None,
|
||||
_("Read access is granted to all teams with access to the event.")),
|
||||
Permission("event.items:write", _("Change products, quotas, and questions"), None,
|
||||
_("Also includes related objects like categories or discounts. Read access is granted to all teams with access to the event.")),
|
||||
Permission("event.orders:read", _("View orders"), None, None),
|
||||
Permission("event.orders:write", _("Change orders"), None, _("This includes the ability to cancel and refund individual orders.")),
|
||||
Permission("event.orders:checkin", _("Check-in orders"), None, None),
|
||||
Permission("event.vouchers:read", _("View vouchers"), None, None),
|
||||
Permission("event.vouchers:write", _("Change vouchers"), None, None),
|
||||
Permission("event:cancel", pgettext_lazy("subevent", "Cancel entire event or date"), None, None),
|
||||
]
|
||||
|
||||
|
||||
@receiver(register_organizer_permissions, dispatch_uid="base_register_default_organizer_permissions")
|
||||
def register_default_organizer_permissions(sender, **kwargs):
|
||||
return [
|
||||
Permission("organizer.events:create", _("Create events"), None, None),
|
||||
Permission("organizer.settings.general:write", _("Change settings"), None,
|
||||
_("This includes access to all organizer-level functionality not listed explicitly below, including plugin settings.")),
|
||||
Permission("organizer.teams:write", _("Change teams"), None,
|
||||
_("This includes the ability to give someone (including oneself) additional permissions. Read access "
|
||||
"is implicitly granted to the same team.")),
|
||||
Permission("organizer.giftcards:read", _("View gift cards"), None, None),
|
||||
Permission("organizer.giftcards:write", _("Change gift cards"), None, None),
|
||||
Permission("organizer.customers:read", _("View customer accounts"), None, None),
|
||||
Permission("organizer.customers:write", _("Change customer accounts"), None, None),
|
||||
Permission("organizer.reusablemedia:read", _("View reusable media"), None,
|
||||
_("This includes access to data of tickets connected to reusable media.")),
|
||||
Permission("organizer.reusablemedia:write", _("Change reusable media"), None, None),
|
||||
Permission("organizer.devices:read", _("View devices and gates"), None, None),
|
||||
Permission("organizer.devices:write", _("Change devices and gates"), None,
|
||||
_("This includes the ability to give access to events and data oneself does not have access to.")),
|
||||
Permission("organizer.seatingplans:write", _("Change seating plans"), None,
|
||||
_("Read access is implicitly given to all teams.")),
|
||||
]
|
||||
@@ -97,6 +97,10 @@ class CartError(Exception):
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class CartPositionError(CartError):
|
||||
pass
|
||||
|
||||
|
||||
error_messages = {
|
||||
'busy': gettext_lazy(
|
||||
'We were not able to process your request completely as the '
|
||||
@@ -106,6 +110,9 @@ error_messages = {
|
||||
'unknown_position': gettext_lazy('Unknown cart position.'),
|
||||
'subevent_required': pgettext_lazy('subevent', 'No date was specified.'),
|
||||
'not_for_sale': gettext_lazy('You selected a product which is not available for sale.'),
|
||||
'positions_removed': gettext_lazy(
|
||||
'Some products can no longer be purchased and have been removed from your cart for the following reason: %s'
|
||||
),
|
||||
'unavailable': gettext_lazy(
|
||||
'Some of the products you selected are no longer available. '
|
||||
'Please see below for details.'
|
||||
@@ -258,6 +265,138 @@ def _get_voucher_availability(event, voucher_use_diff, now_dt, exclude_position_
|
||||
return vouchers_ok, _voucher_depend_on_cart
|
||||
|
||||
|
||||
def _check_position_constraints(
|
||||
event: Event, item: Item, variation: ItemVariation, voucher: Voucher, subevent: SubEvent,
|
||||
seat: Seat, sales_channel: SalesChannel, already_in_cart: bool, cart_is_expired: bool, real_now_dt: datetime,
|
||||
item_requires_seat: bool, is_addon: bool, is_bundled: bool,
|
||||
):
|
||||
"""
|
||||
Checks if a cart position with the given constraints can still be sold. This checks configuration and time-based
|
||||
constraints of item, subevent, and voucher.
|
||||
|
||||
It does NOT
|
||||
- check if quota/voucher/seat are still available
|
||||
- check prices
|
||||
- check memberships
|
||||
- perform any checks that go beyond the single line (like item.max_per_order)
|
||||
"""
|
||||
time_machine_now_dt = time_machine_now(real_now_dt)
|
||||
# Item or variation disabled
|
||||
# Item disabled or unavailable by time
|
||||
if not item.is_available(time_machine_now_dt) or (variation and not variation.is_available(time_machine_now_dt)):
|
||||
raise CartPositionError(error_messages['unavailable'])
|
||||
|
||||
# Invalid media policy for online sale
|
||||
if item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW):
|
||||
mt = MEDIA_TYPES[item.media_type]
|
||||
if not mt.medium_created_by_server:
|
||||
raise CartPositionError(error_messages['media_usage_not_implemented'])
|
||||
elif item.media_policy == Item.MEDIA_POLICY_REUSE:
|
||||
raise CartPositionError(error_messages['media_usage_not_implemented'])
|
||||
|
||||
# Item removed from sales channel
|
||||
if not item.all_sales_channels:
|
||||
if sales_channel.identifier not in (s.identifier for s in item.limit_sales_channels.all()):
|
||||
raise CartPositionError(error_messages['unavailable'])
|
||||
|
||||
# Variation removed from sales channel
|
||||
if variation and not variation.all_sales_channels:
|
||||
if sales_channel.identifier not in (s.identifier for s in variation.limit_sales_channels.all()):
|
||||
raise CartPositionError(error_messages['unavailable'])
|
||||
|
||||
# Item disabled or unavailable by time in subevent
|
||||
if subevent and item.pk in subevent.item_overrides and not subevent.item_overrides[item.pk].is_available(time_machine_now_dt):
|
||||
raise CartPositionError(error_messages['not_for_sale'])
|
||||
|
||||
# Variation disabled or unavailable by time in subevent
|
||||
if subevent and variation and variation.pk in subevent.var_overrides and \
|
||||
not subevent.var_overrides[variation.pk].is_available(time_machine_now_dt):
|
||||
raise CartPositionError(error_messages['not_for_sale'])
|
||||
|
||||
# Item requires a variation (should never happen)
|
||||
if item.has_variations and not variation:
|
||||
raise CartPositionError(error_messages['not_for_sale'])
|
||||
|
||||
# Variation belongs to wrong item (should never happen)
|
||||
if variation and variation.item_id != item.pk:
|
||||
raise CartPositionError(error_messages['not_for_sale'])
|
||||
|
||||
# Voucher does not apply to product
|
||||
if voucher and not voucher.applies_to(item, variation):
|
||||
raise CartPositionError(error_messages['voucher_invalid_item'])
|
||||
|
||||
# Voucher does not apply to seat
|
||||
if voucher and voucher.seat and voucher.seat != seat:
|
||||
raise CartPositionError(error_messages['voucher_invalid_seat'])
|
||||
|
||||
# Voucher does not apply to subevent
|
||||
if voucher and voucher.subevent_id and voucher.subevent_id != subevent.pk:
|
||||
raise CartPositionError(error_messages['voucher_invalid_subevent'])
|
||||
|
||||
# Voucher expired
|
||||
if voucher and voucher.valid_until and voucher.valid_until < time_machine_now_dt:
|
||||
raise CartPositionError(error_messages['voucher_expired'])
|
||||
|
||||
# Subevent has been disabled
|
||||
if subevent and not subevent.active:
|
||||
raise CartPositionError(error_messages['inactive_subevent'])
|
||||
|
||||
# Subevent sale not started
|
||||
if subevent and subevent.effective_presale_start and time_machine_now_dt < subevent.effective_presale_start:
|
||||
raise CartPositionError(error_messages['not_started'])
|
||||
|
||||
# Subevent sale has ended
|
||||
if subevent and subevent.presale_has_ended:
|
||||
raise CartPositionError(error_messages['ended'])
|
||||
|
||||
# Payment for subevent no longer possible
|
||||
if subevent:
|
||||
tlv = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
||||
if tlv:
|
||||
term_last = make_aware(datetime.combine(
|
||||
tlv.datetime(subevent).date(),
|
||||
time(hour=23, minute=59, second=59)
|
||||
), event.timezone)
|
||||
if term_last < time_machine_now_dt:
|
||||
raise CartPositionError(error_messages['payment_ended'])
|
||||
|
||||
# Seat required but no seat given
|
||||
if item_requires_seat and not seat:
|
||||
raise CartPositionError(error_messages['seat_invalid'])
|
||||
|
||||
# Seat given but no seat required
|
||||
if seat and not item_requires_seat:
|
||||
raise CartPositionError(error_messages['seat_forbidden'])
|
||||
|
||||
# Item requires to be add-on but is top-level position
|
||||
if item.category and item.category.is_addon and not is_addon:
|
||||
raise CartPositionError(error_messages['addon_only'])
|
||||
|
||||
# Item requires bundling but is top-level position
|
||||
if item.require_bundling and not is_bundled:
|
||||
raise CartPositionError(error_messages['bundled_only'])
|
||||
|
||||
# Seat for wrong product
|
||||
if seat and seat.product != item:
|
||||
raise CartPositionError(error_messages['seat_invalid'])
|
||||
|
||||
# Seat blocked
|
||||
if seat and seat.blocked and sales_channel.identifier not in event.settings.seating_allow_blocked_seats_for_channel:
|
||||
raise CartPositionError(error_messages['seat_invalid'])
|
||||
|
||||
# Item requires voucher but no voucher given
|
||||
if item.require_voucher and voucher is None and not is_bundled:
|
||||
raise CartPositionError(error_messages['voucher_required'])
|
||||
|
||||
# Item or variation is hidden without voucher but no voucher is given
|
||||
if (
|
||||
(item.hide_without_voucher or (variation and variation.hide_without_voucher)) and
|
||||
(voucher is None or not voucher.show_hidden_items) and
|
||||
not is_bundled
|
||||
):
|
||||
raise CartPositionError(error_messages['voucher_required'])
|
||||
|
||||
|
||||
class CartManager:
|
||||
AddOperation = namedtuple('AddOperation', ('count', 'item', 'variation', 'voucher', 'quotas',
|
||||
'addon_to', 'subevent', 'bundled', 'seat', 'listed_price',
|
||||
@@ -294,6 +433,7 @@ class CartManager:
|
||||
self._widget_data = widget_data or {}
|
||||
self._sales_channel = sales_channel
|
||||
self.num_extended_positions = 0
|
||||
self.price_change_for_extended = False
|
||||
|
||||
if reservation_time:
|
||||
self._reservation_time = reservation_time
|
||||
@@ -421,14 +561,14 @@ class CartManager:
|
||||
if cartsize > limit:
|
||||
raise CartError(error_messages['max_items'] % limit)
|
||||
|
||||
def _check_item_constraints(self, op, current_ops=[]):
|
||||
def _check_item_constraints(self, op):
|
||||
if isinstance(op, (self.AddOperation, self.ExtendOperation)):
|
||||
if not (
|
||||
(isinstance(op, self.AddOperation) and op.addon_to == 'FAKE') or
|
||||
(isinstance(op, self.ExtendOperation) and op.position.is_bundled)
|
||||
):
|
||||
if op.item.require_voucher and op.voucher is None:
|
||||
if getattr(op, 'voucher_ignored', False):
|
||||
if getattr(op, 'voucher_ignored', False): # todo??
|
||||
raise CartError(error_messages['voucher_redeemed'])
|
||||
raise CartError(error_messages['voucher_required'])
|
||||
|
||||
@@ -440,88 +580,39 @@ class CartManager:
|
||||
raise CartError(error_messages['voucher_redeemed'])
|
||||
raise CartError(error_messages['voucher_required'])
|
||||
|
||||
if not op.item.is_available() or (op.variation and not op.variation.is_available()):
|
||||
raise CartError(error_messages['unavailable'])
|
||||
|
||||
if op.item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW):
|
||||
mt = MEDIA_TYPES[op.item.media_type]
|
||||
if not mt.medium_created_by_server:
|
||||
raise CartError(error_messages['media_usage_not_implemented'])
|
||||
elif op.item.media_policy == Item.MEDIA_POLICY_REUSE:
|
||||
raise CartError(error_messages['media_usage_not_implemented'])
|
||||
|
||||
if not op.item.all_sales_channels:
|
||||
if self._sales_channel.identifier not in (s.identifier for s in op.item.limit_sales_channels.all()):
|
||||
raise CartError(error_messages['unavailable'])
|
||||
|
||||
if op.variation and not op.variation.all_sales_channels:
|
||||
if self._sales_channel.identifier not in (s.identifier for s in op.variation.limit_sales_channels.all()):
|
||||
raise CartError(error_messages['unavailable'])
|
||||
|
||||
if op.subevent and op.item.pk in op.subevent.item_overrides and not op.subevent.item_overrides[op.item.pk].is_available():
|
||||
raise CartError(error_messages['not_for_sale'])
|
||||
|
||||
if op.subevent and op.variation and op.variation.pk in op.subevent.var_overrides and \
|
||||
not op.subevent.var_overrides[op.variation.pk].is_available():
|
||||
raise CartError(error_messages['not_for_sale'])
|
||||
|
||||
if op.item.has_variations and not op.variation:
|
||||
raise CartError(error_messages['not_for_sale'])
|
||||
|
||||
if op.variation and op.variation.item_id != op.item.pk:
|
||||
raise CartError(error_messages['not_for_sale'])
|
||||
|
||||
if op.voucher and not op.voucher.applies_to(op.item, op.variation):
|
||||
raise CartError(error_messages['voucher_invalid_item'])
|
||||
|
||||
if op.voucher and op.voucher.seat and op.voucher.seat != op.seat:
|
||||
raise CartError(error_messages['voucher_invalid_seat'])
|
||||
|
||||
if op.voucher and op.voucher.subevent_id and op.voucher.subevent_id != op.subevent.pk:
|
||||
raise CartError(error_messages['voucher_invalid_subevent'])
|
||||
|
||||
if op.subevent and not op.subevent.active:
|
||||
raise CartError(error_messages['inactive_subevent'])
|
||||
|
||||
if op.subevent and op.subevent.presale_start and time_machine_now(self.real_now_dt) < op.subevent.presale_start:
|
||||
raise CartError(error_messages['not_started'])
|
||||
|
||||
if op.subevent and op.subevent.presale_has_ended:
|
||||
raise CartError(error_messages['ended'])
|
||||
|
||||
seated = self._is_seated(op.item, op.subevent)
|
||||
if (
|
||||
seated and (
|
||||
not op.seat or (
|
||||
op.seat.blocked and
|
||||
self._sales_channel.identifier not in self.event.settings.seating_allow_blocked_seats_for_channel
|
||||
)
|
||||
)
|
||||
):
|
||||
raise CartError(error_messages['seat_invalid'])
|
||||
elif op.seat and not seated:
|
||||
raise CartError(error_messages['seat_forbidden'])
|
||||
elif op.seat and op.seat.product != op.item:
|
||||
raise CartError(error_messages['seat_invalid'])
|
||||
elif op.seat and op.count > 1:
|
||||
if op.seat and op.count > 1:
|
||||
raise CartError('Invalid request: A seat can only be bought once.')
|
||||
|
||||
if op.subevent:
|
||||
tlv = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
||||
if tlv:
|
||||
term_last = make_aware(datetime.combine(
|
||||
tlv.datetime(op.subevent).date(),
|
||||
time(hour=23, minute=59, second=59)
|
||||
), self.event.timezone)
|
||||
if term_last < time_machine_now(self.real_now_dt):
|
||||
raise CartError(error_messages['payment_ended'])
|
||||
if isinstance(op, self.AddOperation):
|
||||
is_addon = op.addon_to
|
||||
is_bundled = op.addon_to == "FAKE"
|
||||
else:
|
||||
is_addon = op.position.addon_to
|
||||
is_bundled = op.position.is_bundled
|
||||
|
||||
if isinstance(op, self.AddOperation):
|
||||
if op.item.category and op.item.category.is_addon and not (op.addon_to and op.addon_to != 'FAKE'):
|
||||
raise CartError(error_messages['addon_only'])
|
||||
|
||||
if op.item.require_bundling and not op.addon_to == 'FAKE':
|
||||
raise CartError(error_messages['bundled_only'])
|
||||
try:
|
||||
_check_position_constraints(
|
||||
event=self.event,
|
||||
item=op.item,
|
||||
variation=op.variation,
|
||||
voucher=op.voucher,
|
||||
subevent=op.subevent,
|
||||
seat=op.seat,
|
||||
sales_channel=self._sales_channel,
|
||||
already_in_cart=isinstance(op, self.ExtendOperation),
|
||||
cart_is_expired=isinstance(op, self.ExtendOperation),
|
||||
real_now_dt=self.real_now_dt,
|
||||
item_requires_seat=self._is_seated(op.item, op.subevent),
|
||||
is_addon=is_addon,
|
||||
is_bundled=is_bundled,
|
||||
)
|
||||
# Quota, seat, and voucher availability is checked for in perform_operations
|
||||
# Price changes are checked for in extend_expired_positions
|
||||
except CartPositionError as e:
|
||||
if e.args[0] == error_messages['voucher_required'] and getattr(op, 'voucher_ignored', False):
|
||||
# This is the case where someone clicks +1 on a voucher-only item with a fully redeemed voucher:
|
||||
raise CartPositionError(error_messages['voucher_redeemed'])
|
||||
raise
|
||||
|
||||
def _get_price(self, item: Item, variation: Optional[ItemVariation],
|
||||
voucher: Optional[Voucher], custom_price: Optional[Decimal],
|
||||
@@ -541,7 +632,7 @@ class CartManager:
|
||||
else:
|
||||
raise e
|
||||
|
||||
def extend_expired_positions(self):
|
||||
def _extend_expired_positions(self):
|
||||
requires_seat = Exists(
|
||||
SeatCategoryMapping.objects.filter(
|
||||
Q(product=OuterRef('item'))
|
||||
@@ -604,10 +695,14 @@ class CartManager:
|
||||
quotas=quotas, subevent=cp.subevent, seat=cp.seat, listed_price=listed_price,
|
||||
price_after_voucher=price_after_voucher,
|
||||
)
|
||||
self._check_item_constraints(op)
|
||||
try:
|
||||
self._check_item_constraints(op)
|
||||
except CartPositionError as e:
|
||||
self._operations.append(self.RemoveOperation(position=cp))
|
||||
err = error_messages['positions_removed'] % str(e)
|
||||
|
||||
if cp.voucher:
|
||||
self._voucher_use_diff[cp.voucher] += 2
|
||||
self._voucher_use_diff[cp.voucher] += 1
|
||||
|
||||
self._operations.append(op)
|
||||
return err
|
||||
@@ -797,7 +892,7 @@ class CartManager:
|
||||
custom_price_input_is_net=False,
|
||||
voucher_ignored=False,
|
||||
)
|
||||
self._check_item_constraints(bop, operations)
|
||||
self._check_item_constraints(bop)
|
||||
bundled.append(bop)
|
||||
|
||||
listed_price = get_listed_price(item, variation, subevent)
|
||||
@@ -836,7 +931,7 @@ class CartManager:
|
||||
custom_price_input_is_net=self.event.settings.display_net_prices,
|
||||
voucher_ignored=voucher_ignored,
|
||||
)
|
||||
self._check_item_constraints(op, operations)
|
||||
self._check_item_constraints(op)
|
||||
operations.append(op)
|
||||
|
||||
self._quota_diff.update(quota_diff)
|
||||
@@ -975,7 +1070,7 @@ class CartManager:
|
||||
custom_price_input_is_net=self.event.settings.display_net_prices,
|
||||
voucher_ignored=False,
|
||||
)
|
||||
self._check_item_constraints(op, operations)
|
||||
self._check_item_constraints(op)
|
||||
operations.append(op)
|
||||
|
||||
# Check constraints on the add-on combinations
|
||||
@@ -1172,7 +1267,9 @@ class CartManager:
|
||||
op.position.delete()
|
||||
|
||||
elif isinstance(op, (self.AddOperation, self.ExtendOperation)):
|
||||
# Create a CartPosition for as much items as we can
|
||||
if isinstance(op, self.ExtendOperation) and (op.position.pk in deleted_positions or not op.position.pk):
|
||||
continue # Already deleted in other operation
|
||||
# Create a CartPosition for as many items as we can
|
||||
requested_count = quota_available_count = voucher_available_count = op.count
|
||||
|
||||
if op.seat:
|
||||
@@ -1343,6 +1440,8 @@ class CartManager:
|
||||
addons.delete()
|
||||
op.position.delete()
|
||||
elif available_count == 1:
|
||||
if op.price_after_voucher != op.position.price_after_voucher:
|
||||
self.price_change_for_extended = True
|
||||
op.position.expires = self._expiry
|
||||
op.position.max_extend = self._max_expiry_extend
|
||||
op.position.listed_price = op.listed_price
|
||||
@@ -1444,15 +1543,24 @@ class CartManager:
|
||||
|
||||
return diff
|
||||
|
||||
def _remove_parents_if_bundles_are_removed(self):
|
||||
removed_positions = {op.position.pk for op in self._operations if isinstance(op, self.RemoveOperation)}
|
||||
for op in self._operations:
|
||||
if isinstance(op, self.RemoveOperation):
|
||||
if op.position.is_bundled and op.position.addon_to_id not in removed_positions:
|
||||
self._operations.append(self.RemoveOperation(position=op.position.addon_to))
|
||||
removed_positions.add(op.position.addon_to_id)
|
||||
|
||||
def commit(self):
|
||||
self._check_presale_dates()
|
||||
self._check_max_cart_size()
|
||||
|
||||
err = self._delete_out_of_timeframe()
|
||||
err = self.extend_expired_positions() or err
|
||||
err = self._extend_expired_positions() or err
|
||||
err = err or self._check_min_per_voucher()
|
||||
|
||||
self._extend_expiry_of_valid_existing_positions()
|
||||
self._remove_parents_if_bundles_are_removed()
|
||||
err = self._perform_operations() or err
|
||||
self.recompute_final_prices_and_taxes()
|
||||
|
||||
@@ -1708,7 +1816,12 @@ def extend_cart_reservation(self, event: Event, cart_id: str=None, locale='en',
|
||||
try:
|
||||
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
||||
cm.commit()
|
||||
return {"success": cm.num_extended_positions, "expiry": cm._expiry, "max_expiry_extend": cm._max_expiry_extend}
|
||||
return {
|
||||
"success": cm.num_extended_positions,
|
||||
"expiry": cm._expiry,
|
||||
"max_expiry_extend": cm._max_expiry_extend,
|
||||
"price_changed": cm.price_change_for_extended,
|
||||
}
|
||||
except LockTimeoutException:
|
||||
self.retry()
|
||||
except (MaxRetriesExceededError, LockTimeoutException):
|
||||
|
||||
@@ -106,7 +106,7 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
|
||||
device = Device.objects.get(pk=device)
|
||||
if token:
|
||||
device = TeamAPIToken.objects.get(pk=token)
|
||||
allowed_events = (device or token or user).get_events_with_permission('can_view_orders')
|
||||
allowed_events = (device or token or user).get_events_with_permission('event.orders:read')
|
||||
if user and staff_session:
|
||||
allowed_events = organizer.events.all()
|
||||
|
||||
@@ -291,7 +291,7 @@ def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter,
|
||||
def scheduled_organizer_export(self, organizer: Organizer, schedule: int) -> None:
|
||||
schedule = organizer.scheduled_exports.get(pk=schedule)
|
||||
|
||||
allowed_events = schedule.owner.get_events_with_permission('can_view_orders')
|
||||
allowed_events = schedule.owner.get_events_with_permission('event.orders:read')
|
||||
if schedule.export_form_data.get('events') is not None and not schedule.export_form_data.get('all_events'):
|
||||
if isinstance(schedule.export_form_data['events'][0], str):
|
||||
events = allowed_events.filter(slug__in=schedule.export_form_data.get('events'), organizer=organizer)
|
||||
@@ -346,7 +346,7 @@ def scheduled_event_export(self, event: Event, schedule: int) -> None:
|
||||
exporter = ex
|
||||
break
|
||||
|
||||
has_permission = schedule.owner.is_active and schedule.owner.has_event_permission(event.organizer, event, 'can_view_orders')
|
||||
has_permission = schedule.owner.is_active and schedule.owner.has_event_permission(event.organizer, event, 'event.orders:read')
|
||||
|
||||
_run_scheduled_export(
|
||||
schedule,
|
||||
|
||||
@@ -81,7 +81,7 @@ from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
||||
from pretix.base.payment import GiftCardPayment, PaymentException
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.secrets import assign_ticket_secret
|
||||
from pretix.base.services import tickets
|
||||
from pretix.base.services import cart, tickets
|
||||
from pretix.base.services.invoices import (
|
||||
generate_cancellation, generate_invoice, invoice_qualified,
|
||||
invoice_transmission_separately, order_invoice_transmission_separately,
|
||||
@@ -130,6 +130,9 @@ class OrderError(Exception):
|
||||
|
||||
|
||||
error_messages = {
|
||||
'positions_removed': gettext_lazy(
|
||||
'Some products can no longer be purchased and have been removed from your cart for the following reason: %s'
|
||||
),
|
||||
'unavailable': gettext_lazy(
|
||||
'Some of the products you selected were no longer available. '
|
||||
'Please see below for details.'
|
||||
@@ -182,14 +185,6 @@ error_messages = {
|
||||
'The voucher code used for one of the items in your cart is not valid for this item. We removed this item from your cart.'
|
||||
),
|
||||
'voucher_required': gettext_lazy('You need a valid voucher code to order one of the products.'),
|
||||
'some_subevent_not_started': gettext_lazy(
|
||||
'The booking period for one of the events in your cart has not yet started. The '
|
||||
'affected positions have been removed from your cart.'
|
||||
),
|
||||
'some_subevent_ended': gettext_lazy(
|
||||
'The booking period for one of the events in your cart has ended. The affected '
|
||||
'positions have been removed from your cart.'
|
||||
),
|
||||
'seat_invalid': gettext_lazy('One of the seats in your order was invalid, we removed the position from your cart.'),
|
||||
'seat_unavailable': gettext_lazy('One of the seats in your order has been taken in the meantime, we removed the position from your cart.'),
|
||||
'country_blocked': gettext_lazy('One of the selected products is not available in the selected country.'),
|
||||
@@ -744,12 +739,37 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
||||
deleted_positions.add(cp.pk)
|
||||
cp.delete()
|
||||
|
||||
sorted_positions = sorted(positions, key=lambda c: (-int(c.is_bundled), c.pk))
|
||||
sorted_positions = list(sorted(positions, key=lambda c: (-int(c.is_bundled), c.pk)))
|
||||
|
||||
for cp in sorted_positions:
|
||||
cp._cached_quotas = list(cp.quotas)
|
||||
|
||||
for cp in sorted_positions:
|
||||
try:
|
||||
cart._check_position_constraints(
|
||||
event=event,
|
||||
item=cp.item,
|
||||
variation=cp.variation,
|
||||
voucher=cp.voucher,
|
||||
subevent=cp.subevent,
|
||||
seat=cp.seat,
|
||||
sales_channel=sales_channel,
|
||||
already_in_cart=True,
|
||||
cart_is_expired=cp.expires < now_dt,
|
||||
real_now_dt=now_dt,
|
||||
item_requires_seat=cp.requires_seat,
|
||||
is_addon=bool(cp.addon_to_id),
|
||||
is_bundled=bool(cp.addon_to_id) and cp.is_bundled,
|
||||
)
|
||||
# Quota, seat, and voucher availability is checked for below
|
||||
# Prices are checked for below
|
||||
# Memberships are checked in _create_order
|
||||
except cart.CartPositionError as e:
|
||||
err = error_messages['positions_removed'] % str(e)
|
||||
delete(cp)
|
||||
|
||||
# Create locks
|
||||
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions] # eliminate deleted
|
||||
if any(cp.expires < now() + timedelta(seconds=LOCK_TRUST_WINDOW) for cp in sorted_positions):
|
||||
# No need to perform any locking if the cart positions still guarantee everything long enough.
|
||||
full_lock_required = any(
|
||||
@@ -774,15 +794,12 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
||||
|
||||
# Check availability
|
||||
for i, cp in enumerate(sorted_positions):
|
||||
if cp.pk in deleted_positions:
|
||||
if cp.pk in deleted_positions or not cp.pk:
|
||||
continue
|
||||
|
||||
if not cp.item.is_available() or (cp.variation and not cp.variation.is_available()):
|
||||
err = err or error_messages['unavailable']
|
||||
delete(cp)
|
||||
continue
|
||||
quotas = cp._cached_quotas
|
||||
|
||||
# Product per order limits
|
||||
products_seen[cp.item] += 1
|
||||
if cp.item.max_per_order and products_seen[cp.item] > cp.item.max_per_order:
|
||||
err = error_messages['max_items_per_product'] % {
|
||||
@@ -792,6 +809,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
||||
delete(cp)
|
||||
break
|
||||
|
||||
# Voucher availability
|
||||
if cp.voucher:
|
||||
v_usages[cp.voucher] += 1
|
||||
if cp.voucher not in v_avail:
|
||||
@@ -806,48 +824,14 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
if cp.subevent and cp.subevent.presale_start and time_machine_now_dt < cp.subevent.presale_start:
|
||||
err = err or error_messages['some_subevent_not_started']
|
||||
delete(cp)
|
||||
break
|
||||
|
||||
if cp.subevent:
|
||||
tlv = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
||||
if tlv:
|
||||
term_last = make_aware(datetime.combine(
|
||||
tlv.datetime(cp.subevent).date(),
|
||||
time(hour=23, minute=59, second=59)
|
||||
), event.timezone)
|
||||
if term_last < time_machine_now_dt:
|
||||
err = err or error_messages['some_subevent_ended']
|
||||
delete(cp)
|
||||
break
|
||||
|
||||
if cp.subevent and cp.subevent.presale_has_ended:
|
||||
err = err or error_messages['some_subevent_ended']
|
||||
delete(cp)
|
||||
break
|
||||
|
||||
if (cp.requires_seat and not cp.seat) or (cp.seat and not cp.requires_seat) or (cp.seat and cp.seat.product != cp.item) or cp.seat in seats_seen:
|
||||
# Check duplicate seats in order
|
||||
if cp.seat in seats_seen:
|
||||
err = err or error_messages['seat_invalid']
|
||||
delete(cp)
|
||||
break
|
||||
|
||||
if cp.seat:
|
||||
seats_seen.add(cp.seat)
|
||||
|
||||
if cp.item.require_voucher and cp.voucher is None and not cp.is_bundled:
|
||||
delete(cp)
|
||||
err = err or error_messages['voucher_required']
|
||||
break
|
||||
|
||||
if (cp.item.hide_without_voucher or (cp.variation and cp.variation.hide_without_voucher)) and (
|
||||
cp.voucher is None or not cp.voucher.show_hidden_items or not cp.voucher.applies_to(cp.item, cp.variation)
|
||||
) and not cp.is_bundled:
|
||||
delete(cp)
|
||||
err = error_messages['voucher_required']
|
||||
break
|
||||
|
||||
if cp.seat:
|
||||
# Unlike quotas (which we blindly trust as long as the position is not expired), we check seats every
|
||||
# time, since we absolutely can not overbook a seat.
|
||||
if not cp.seat.is_available(ignore_cart=cp, ignore_voucher_id=cp.voucher_id, sales_channel=sales_channel.identifier):
|
||||
@@ -855,34 +839,13 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
if cp.expires >= now_dt and not cp.voucher:
|
||||
# Other checks are not necessary
|
||||
continue
|
||||
|
||||
# Check useful quota configuration
|
||||
if len(quotas) == 0:
|
||||
err = err or error_messages['unavailable']
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
if cp.subevent and cp.item.pk in cp.subevent.item_overrides and not cp.subevent.item_overrides[cp.item.pk].is_available(time_machine_now_dt):
|
||||
err = err or error_messages['unavailable']
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
if cp.subevent and cp.variation and cp.variation.pk in cp.subevent.var_overrides and \
|
||||
not cp.subevent.var_overrides[cp.variation.pk].is_available(time_machine_now_dt):
|
||||
err = err or error_messages['unavailable']
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
if cp.voucher:
|
||||
if cp.voucher.valid_until and cp.voucher.valid_until < time_machine_now_dt:
|
||||
err = err or error_messages['voucher_expired']
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
quota_ok = True
|
||||
|
||||
ignore_all_quotas = cp.expires >= now_dt or (
|
||||
cp.voucher and (
|
||||
cp.voucher.allow_ignore_quota or (cp.voucher.block_quota and cp.voucher.quota is None)
|
||||
@@ -914,7 +877,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
||||
})
|
||||
|
||||
# Check prices
|
||||
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions]
|
||||
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions] # eliminate deleted
|
||||
old_total = sum(cp.price for cp in sorted_positions)
|
||||
for i, cp in enumerate(sorted_positions):
|
||||
if cp.listed_price is None:
|
||||
@@ -945,7 +908,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions]
|
||||
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions] # eliminate deleted
|
||||
discount_results = apply_discounts(
|
||||
event,
|
||||
sales_channel.identifier,
|
||||
@@ -1667,7 +1630,7 @@ class OrderChangeManager:
|
||||
MembershipOperation = namedtuple('MembershipOperation', ('position', 'membership'))
|
||||
CancelOperation = namedtuple('CancelOperation', ('position', 'price_diff'))
|
||||
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent', 'seat', 'membership',
|
||||
'valid_from', 'valid_until', 'is_bundled'))
|
||||
'valid_from', 'valid_until', 'is_bundled', 'result'))
|
||||
SplitOperation = namedtuple('SplitOperation', ('position',))
|
||||
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value', 'price_diff'))
|
||||
AddFeeOperation = namedtuple('AddFeeOperation', ('fee', 'price_diff'))
|
||||
@@ -1679,6 +1642,18 @@ class OrderChangeManager:
|
||||
AddBlockOperation = namedtuple('AddBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
|
||||
RemoveBlockOperation = namedtuple('RemoveBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
|
||||
|
||||
class AddPositionResult:
|
||||
_position: Optional[OrderPosition]
|
||||
|
||||
def __init__(self):
|
||||
self._position = None
|
||||
|
||||
@property
|
||||
def position(self) -> OrderPosition:
|
||||
if self._position is None:
|
||||
raise RuntimeError("Order position has not been created yet. Call commit() first on OrderChangeManager.")
|
||||
return self._position
|
||||
|
||||
def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True, allow_blocked_seats=False):
|
||||
self.order = order
|
||||
self.user = user
|
||||
@@ -1883,7 +1858,7 @@ class OrderChangeManager:
|
||||
|
||||
def add_position(self, item: Item, variation: ItemVariation, price: Decimal, addon_to: OrderPosition = None,
|
||||
subevent: SubEvent = None, seat: Seat = None, membership: Membership = None,
|
||||
valid_from: datetime = None, valid_until: datetime = None):
|
||||
valid_from: datetime = None, valid_until: datetime = None) -> 'OrderChangeManager.AddPositionResult':
|
||||
if isinstance(seat, str):
|
||||
if not seat:
|
||||
seat = None
|
||||
@@ -1942,8 +1917,11 @@ class OrderChangeManager:
|
||||
self._quotadiff.update(new_quotas)
|
||||
if seat:
|
||||
self._seatdiff.update([seat])
|
||||
|
||||
result = self.AddPositionResult()
|
||||
self._operations.append(self.AddOperation(item, variation, price, addon_to, subevent, seat, membership,
|
||||
valid_from, valid_until, is_bundled))
|
||||
valid_from, valid_until, is_bundled, result))
|
||||
return result
|
||||
|
||||
def split(self, position: OrderPosition):
|
||||
if self.order.event.settings.invoice_include_free or position.price != Decimal('0.00'):
|
||||
@@ -2562,6 +2540,7 @@ class OrderChangeManager:
|
||||
'valid_from': op.valid_from.isoformat() if op.valid_from else None,
|
||||
'valid_until': op.valid_until.isoformat() if op.valid_until else None,
|
||||
})
|
||||
op.result._position = pos
|
||||
elif isinstance(op, self.SplitOperation):
|
||||
position = position_cache.setdefault(op.position.pk, op.position)
|
||||
split_positions.append(position)
|
||||
|
||||
@@ -801,7 +801,11 @@ def get_sample_context(event, context_parameters, rich=True):
|
||||
sample = v.render_sample(event)
|
||||
if isinstance(sample, PlainHtmlAlternativeString):
|
||||
context_dict[k] = PlainHtmlAlternativeString(
|
||||
sample.plain,
|
||||
'<{el} class="placeholder" title="{title}">{plain}</{el}>'.format(
|
||||
el='span',
|
||||
title=lbl,
|
||||
plain=escape(sample.plain),
|
||||
),
|
||||
'<{el} class="placeholder placeholder-html" title="{title}">{html}</{el}>'.format(
|
||||
el='div' if sample.is_block else 'span',
|
||||
title=lbl,
|
||||
|
||||
@@ -331,6 +331,7 @@ DEFAULTS = {
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.tax:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Show net prices instead of gross prices in the product list"),
|
||||
help_text=_("Independent of your choice, the cart will show gross prices as this is the price that needs to be "
|
||||
@@ -478,6 +479,7 @@ DEFAULTS = {
|
||||
'type': str,
|
||||
'form_class': forms.ChoiceField,
|
||||
'serializer_class': serializers.ChoiceField,
|
||||
'write_permission': 'event.settings.tax:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Rounding of taxes"),
|
||||
widget=forms.RadioSelect,
|
||||
@@ -497,15 +499,17 @@ DEFAULTS = {
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Ask for invoice address"),
|
||||
)
|
||||
),
|
||||
},
|
||||
'invoice_address_not_asked_free': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_('Do not ask for invoice address if an order is free'),
|
||||
)
|
||||
@@ -515,6 +519,7 @@ DEFAULTS = {
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Require customer name"),
|
||||
)
|
||||
@@ -524,6 +529,7 @@ DEFAULTS = {
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Show attendee names on invoices"),
|
||||
)
|
||||
@@ -533,6 +539,7 @@ DEFAULTS = {
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Show event location on invoices"),
|
||||
help_text=_("The event location will be shown below the list of products if it is the same for all "
|
||||
@@ -544,6 +551,7 @@ DEFAULTS = {
|
||||
'type': str,
|
||||
'form_class': forms.ChoiceField,
|
||||
'serializer_class': serializers.ChoiceField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Show exchange rates"),
|
||||
widget=forms.RadioSelect,
|
||||
@@ -567,6 +575,7 @@ DEFAULTS = {
|
||||
'default': 'False',
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'type': bool,
|
||||
'form_kwargs': dict(
|
||||
label=_("Require invoice address"),
|
||||
@@ -577,6 +586,7 @@ DEFAULTS = {
|
||||
'default': 'False',
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'type': bool,
|
||||
'form_kwargs': dict(
|
||||
label=_("Require a business address"),
|
||||
@@ -589,6 +599,7 @@ DEFAULTS = {
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Ask for beneficiary"),
|
||||
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_asked'}),
|
||||
@@ -599,6 +610,7 @@ DEFAULTS = {
|
||||
'type': LazyI18nString,
|
||||
'form_class': I18nFormField,
|
||||
'serializer_class': I18nField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Custom recipient field label"),
|
||||
widget=I18nTextInput,
|
||||
@@ -614,6 +626,7 @@ DEFAULTS = {
|
||||
'type': LazyI18nString,
|
||||
'form_class': I18nFormField,
|
||||
'serializer_class': I18nField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Custom recipient field help text"),
|
||||
widget=I18nTextInput,
|
||||
@@ -626,6 +639,7 @@ DEFAULTS = {
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Ask for VAT ID"),
|
||||
help_text=format_lazy(
|
||||
@@ -641,6 +655,7 @@ DEFAULTS = {
|
||||
'type': list,
|
||||
'form_class': forms.MultipleChoiceField,
|
||||
'serializer_class': serializers.MultipleChoiceField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'serializer_kwargs': dict(
|
||||
choices=lazy(
|
||||
lambda *args: sorted([(cc, gettext(Country(cc).name)) for cc in VAT_ID_COUNTRIES], key=lambda c: c[1]),
|
||||
@@ -668,6 +683,7 @@ DEFAULTS = {
|
||||
'type': LazyI18nString,
|
||||
'form_class': I18nFormField,
|
||||
'serializer_class': I18nField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Invoice address explanation"),
|
||||
widget=I18nMarkdownTextarea,
|
||||
@@ -680,6 +696,7 @@ DEFAULTS = {
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Show paid amount on partially paid invoices"),
|
||||
help_text=_("If an invoice has already been paid partially, this option will add the paid and pending "
|
||||
@@ -691,6 +708,7 @@ DEFAULTS = {
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Show free products on invoices"),
|
||||
help_text=_("Note that invoices will never be generated for orders that contain only free "
|
||||
@@ -702,6 +720,7 @@ DEFAULTS = {
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Show expiration date of order"),
|
||||
help_text=_("The expiration date will not be shown if the invoice is generated after the order is paid."),
|
||||
@@ -713,6 +732,7 @@ DEFAULTS = {
|
||||
'form_class': forms.IntegerField,
|
||||
'serializer_class': serializers.IntegerField,
|
||||
'serializer_kwargs': dict(),
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Minimum length of invoice number after prefix"),
|
||||
help_text=_("The part of your invoice number after your prefix will be filled up with leading zeros up to this length, e.g. INV-001 or INV-00001."),
|
||||
@@ -726,6 +746,7 @@ DEFAULTS = {
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Generate invoices with consecutive numbers"),
|
||||
help_text=_("If deactivated, the order code will be used in the invoice number."),
|
||||
@@ -736,6 +757,7 @@ DEFAULTS = {
|
||||
'type': str,
|
||||
'form_class': forms.CharField,
|
||||
'serializer_class': serializers.CharField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Invoice number prefix"),
|
||||
help_text=_("This will be prepended to invoice numbers. If you leave this field empty, your event slug will "
|
||||
@@ -763,6 +785,7 @@ DEFAULTS = {
|
||||
'type': str,
|
||||
'form_class': forms.CharField,
|
||||
'serializer_class': serializers.CharField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Invoice number prefix for cancellations"),
|
||||
help_text=_("This will be prepended to invoice numbers of cancellations. If you leave this field empty, "
|
||||
@@ -786,6 +809,7 @@ DEFAULTS = {
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Highlight order code to make it stand out visibly"),
|
||||
help_text=_("Only respected by some invoice renderers."),
|
||||
@@ -797,6 +821,7 @@ DEFAULTS = {
|
||||
'form_class': forms.ChoiceField,
|
||||
'serializer_class': serializers.ChoiceField,
|
||||
'serializer_kwargs': lambda: dict(**invoice_font_kwargs()),
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': lambda: dict(
|
||||
label=_('Font'),
|
||||
help_text=_("Only respected by some invoice renderers."),
|
||||
@@ -807,6 +832,7 @@ DEFAULTS = {
|
||||
'invoice_renderer': {
|
||||
'default': 'classic', # default for new events is 'modern1'
|
||||
'type': str,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
},
|
||||
'ticket_secret_generator': {
|
||||
'default': 'random',
|
||||
@@ -883,6 +909,7 @@ DEFAULTS = {
|
||||
'type': LazyI18nString,
|
||||
'form_class': I18nFormField,
|
||||
'serializer_class': I18nField,
|
||||
'write_permission': 'event.settings.payment:write',
|
||||
'form_kwargs': dict(
|
||||
widget=I18nMarkdownTextarea,
|
||||
widget_kwargs={'attrs': {
|
||||
@@ -904,6 +931,7 @@ DEFAULTS = {
|
||||
('minutes', _("in minutes"))
|
||||
),
|
||||
),
|
||||
'write_permission': 'event.settings.payment:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Set payment term"),
|
||||
widget=forms.RadioSelect,
|
||||
@@ -921,6 +949,7 @@ DEFAULTS = {
|
||||
'type': int,
|
||||
'form_class': forms.IntegerField,
|
||||
'serializer_class': serializers.IntegerField,
|
||||
'write_permission': 'event.settings.payment:write',
|
||||
'form_kwargs': dict(
|
||||
label=_('Payment term in days'),
|
||||
widget=forms.NumberInput(
|
||||
@@ -946,6 +975,7 @@ DEFAULTS = {
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.payment:write',
|
||||
'form_kwargs': dict(
|
||||
label=_('Only end payment terms on weekdays'),
|
||||
help_text=_("If this is activated and the payment term of any order ends on a Saturday or Sunday, it will be "
|
||||
@@ -963,6 +993,7 @@ DEFAULTS = {
|
||||
'type': int,
|
||||
'form_class': forms.IntegerField,
|
||||
'serializer_class': serializers.IntegerField,
|
||||
'write_permission': 'event.settings.payment:write',
|
||||
'form_kwargs': dict(
|
||||
label=_('Payment term in minutes'),
|
||||
help_text=_("The number of minutes after placing an order the user has to pay to preserve their reservation. "
|
||||
@@ -987,6 +1018,7 @@ DEFAULTS = {
|
||||
'type': RelativeDateWrapper,
|
||||
'form_class': RelativeDateField,
|
||||
'serializer_class': SerializerRelativeDateField,
|
||||
'write_permission': 'event.settings.payment:write',
|
||||
'form_kwargs': dict(
|
||||
label=_('Last date of payments'),
|
||||
help_text=_("The last date any payments are accepted. This has precedence over the terms "
|
||||
@@ -999,6 +1031,7 @@ DEFAULTS = {
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.payment:write',
|
||||
'form_kwargs': dict(
|
||||
label=_('Automatically expire unpaid orders'),
|
||||
help_text=_("If checked, all unpaid orders will automatically go from 'pending' to 'expired' "
|
||||
@@ -1011,6 +1044,7 @@ DEFAULTS = {
|
||||
'type': int,
|
||||
'form_class': forms.IntegerField,
|
||||
'serializer_class': serializers.IntegerField,
|
||||
'write_permission': 'event.settings.payment:write',
|
||||
'form_kwargs': dict(
|
||||
label=_('Expiration delay'),
|
||||
help_text=_("The order will only actually expire this many days after the expiration date communicated "
|
||||
@@ -1033,6 +1067,7 @@ DEFAULTS = {
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.payment:write',
|
||||
'form_kwargs': dict(
|
||||
label=_('Hide "payment pending" state on customer-facing pages'),
|
||||
help_text=_("The payment instructions panel will still be shown to the primary customer, but no indication "
|
||||
@@ -1044,9 +1079,11 @@ DEFAULTS = {
|
||||
'default': 'True',
|
||||
'type': bool,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.payment:write',
|
||||
},
|
||||
'payment_giftcard_public_name': {
|
||||
'default': LazyI18nString.from_gettext(gettext_noop('Gift card')),
|
||||
'write_permission': 'event.settings.payment:write',
|
||||
'type': LazyI18nString
|
||||
},
|
||||
'payment_giftcard_public_description': {
|
||||
@@ -1055,10 +1092,12 @@ DEFAULTS = {
|
||||
'enough credit to pay for the full order, you will be shown this page again and you can either '
|
||||
'redeem another gift card or select a different payment method for the difference.'
|
||||
)),
|
||||
'write_permission': 'event.settings.payment:write',
|
||||
'type': LazyI18nString
|
||||
},
|
||||
'payment_resellers__restrict_to_sales_channels': {
|
||||
'default': ['resellers'],
|
||||
'write_permission': 'event.settings.payment:write',
|
||||
'type': list
|
||||
},
|
||||
'payment_term_accept_late': {
|
||||
@@ -1066,6 +1105,7 @@ DEFAULTS = {
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.payment:write',
|
||||
'form_kwargs': dict(
|
||||
label=_('Accept late payments'),
|
||||
help_text=_("Accept payments for orders even when they are in 'expired' state as long as enough "
|
||||
@@ -1095,6 +1135,7 @@ DEFAULTS = {
|
||||
('none', _('Charge no taxes')),
|
||||
),
|
||||
),
|
||||
'write_permission': 'event.settings.payment:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Tax handling on payment fees"),
|
||||
widget=forms.RadioSelect,
|
||||
@@ -1141,6 +1182,7 @@ DEFAULTS = {
|
||||
('paid', _('Automatically on payment or when required by payment method')),
|
||||
),
|
||||
),
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Generate invoices"),
|
||||
widget=forms.RadioSelect,
|
||||
@@ -1169,6 +1211,7 @@ DEFAULTS = {
|
||||
('invoice_date', _('Invoice date')),
|
||||
),
|
||||
),
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Date of service"),
|
||||
widget=forms.RadioSelect,
|
||||
@@ -1189,6 +1232,7 @@ DEFAULTS = {
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Automatically cancel and reissue invoice on address changes"),
|
||||
help_text=_("If customers change their invoice address on an existing order, the invoice will "
|
||||
@@ -1201,6 +1245,7 @@ DEFAULTS = {
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Allow to update existing invoices"),
|
||||
help_text=_("By default, invoices can never again be changed once they are issued. In most countries, we "
|
||||
@@ -1210,6 +1255,7 @@ DEFAULTS = {
|
||||
},
|
||||
'invoice_generate_sales_channels': {
|
||||
'default': json.dumps(['web']),
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'type': list
|
||||
},
|
||||
'invoice_address_from': {
|
||||
@@ -1217,6 +1263,7 @@ DEFAULTS = {
|
||||
'type': str,
|
||||
'form_class': forms.CharField,
|
||||
'serializer_class': serializers.CharField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Address line"),
|
||||
widget=forms.Textarea(attrs={
|
||||
@@ -1232,6 +1279,7 @@ DEFAULTS = {
|
||||
'type': str,
|
||||
'form_class': forms.CharField,
|
||||
'serializer_class': serializers.CharField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
max_length=190,
|
||||
label=_("Company name"),
|
||||
@@ -1242,6 +1290,7 @@ DEFAULTS = {
|
||||
'type': str,
|
||||
'form_class': forms.CharField,
|
||||
'serializer_class': serializers.CharField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': '12345'
|
||||
@@ -1255,6 +1304,7 @@ DEFAULTS = {
|
||||
'type': str,
|
||||
'form_class': forms.CharField,
|
||||
'serializer_class': serializers.CharField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _('Random City')
|
||||
@@ -1271,6 +1321,7 @@ DEFAULTS = {
|
||||
'serializer_kwargs': {
|
||||
'choices': [('', '')],
|
||||
},
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': {
|
||||
"label": pgettext_lazy('address', 'State'),
|
||||
'choices': [('', '')],
|
||||
@@ -1282,6 +1333,7 @@ DEFAULTS = {
|
||||
'form_class': forms.ChoiceField,
|
||||
'serializer_class': serializers.ChoiceField,
|
||||
'serializer_kwargs': lambda: dict(**country_choice_kwargs()),
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': lambda: dict(
|
||||
label=_('Country'),
|
||||
widget=forms.Select(attrs={
|
||||
@@ -1295,6 +1347,7 @@ DEFAULTS = {
|
||||
'type': str,
|
||||
'form_class': forms.CharField,
|
||||
'serializer_class': serializers.CharField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Domestic tax ID"),
|
||||
help_text=_("e.g. tax number in Germany, ABN in Australia, …"),
|
||||
@@ -1306,6 +1359,7 @@ DEFAULTS = {
|
||||
'type': str,
|
||||
'form_class': forms.CharField,
|
||||
'serializer_class': serializers.CharField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("EU VAT ID"),
|
||||
max_length=190,
|
||||
@@ -1316,6 +1370,7 @@ DEFAULTS = {
|
||||
'type': LazyI18nString,
|
||||
'form_class': I18nFormField,
|
||||
'serializer_class': I18nField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {
|
||||
@@ -1333,6 +1388,7 @@ DEFAULTS = {
|
||||
'type': LazyI18nString,
|
||||
'form_class': I18nFormField,
|
||||
'serializer_class': I18nField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {
|
||||
@@ -1350,6 +1406,7 @@ DEFAULTS = {
|
||||
'type': LazyI18nString,
|
||||
'form_class': I18nFormField,
|
||||
'serializer_class': I18nField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {
|
||||
@@ -1364,6 +1421,7 @@ DEFAULTS = {
|
||||
},
|
||||
'invoice_language': {
|
||||
'default': '__user__',
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'type': str
|
||||
},
|
||||
'invoice_email_attachment': {
|
||||
@@ -1371,6 +1429,7 @@ DEFAULTS = {
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Attach invoices to emails"),
|
||||
help_text=_("If invoices are automatically generated for all orders, they will be attached to the order "
|
||||
@@ -1384,6 +1443,7 @@ DEFAULTS = {
|
||||
'type': str,
|
||||
'form_class': forms.CharField,
|
||||
'serializer_class': serializers.CharField,
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
'form_kwargs': dict(
|
||||
label=_("Email address to receive a copy of each invoice"),
|
||||
help_text=_("Each newly created invoice will be sent to this email address shortly after creation. You can "
|
||||
@@ -3215,7 +3275,8 @@ Your {organizer} team""")) # noqa: W291
|
||||
'image/png', 'image/jpeg', 'image/gif'
|
||||
],
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||
)
|
||||
),
|
||||
'write_permission': 'event.settings.invoicing:write',
|
||||
},
|
||||
'frontpage_text': {
|
||||
'default': '',
|
||||
|
||||
@@ -561,6 +561,18 @@ however for this signal, the ``sender`` **may also be None** to allow creating t
|
||||
notification settings!
|
||||
"""
|
||||
|
||||
register_event_permissions = GlobalSignal()
|
||||
"""
|
||||
This signal is sent out to get all known permissions. Receivers should return an
|
||||
instance of pretix.base.permissions.Permission or a list of such instances.
|
||||
"""
|
||||
|
||||
register_organizer_permissions = GlobalSignal()
|
||||
"""
|
||||
This signal is sent out to get all known permissions. Receivers should return an
|
||||
instance of pretix.base.permissions.Permission or a list of such instances.
|
||||
"""
|
||||
|
||||
notification = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``logentry_id``, ``notification_type``
|
||||
@@ -1098,6 +1110,9 @@ api_event_settings_fields = EventPluginSignal()
|
||||
This signal is sent out to collect serializable settings fields for the API. You are expected to
|
||||
return a dictionary mapping names of attributes in the settings store to DRF serializer field instances.
|
||||
|
||||
These are readable for all users with access to the events, therefore secrets made in the settings store
|
||||
should not be included!
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
|
||||
65
src/pretix/base/templatetags/html_time.py
Normal file
65
src/pretix/base/templatetags/html_time.py
Normal file
@@ -0,0 +1,65 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from datetime import datetime
|
||||
|
||||
from django import template
|
||||
from django.utils.html import format_html
|
||||
from django.utils.timezone import get_current_timezone
|
||||
|
||||
from pretix.base.i18n import LazyExpiresDate
|
||||
from pretix.helpers.templatetags.date_fast import date_fast
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def html_time(value: datetime, dt_format: str = "SHORT_DATE_FORMAT", **kwargs):
|
||||
"""
|
||||
Building a <time datetime='{html-datetime}'>{human-readable datetime}</time> html string,
|
||||
where the html-datetime as well as the human-readable datetime can be set
|
||||
to a value from django's FORMAT_SETTINGS or "format_expires".
|
||||
|
||||
If attr_fmt isn’t provided, it will be set to isoformat.
|
||||
|
||||
Usage example:
|
||||
{% html_time event_start "SHORT_DATETIME_FORMAT" %}
|
||||
or
|
||||
{% html_time event_start "TIME_FORMAT" attr_fmt="H:i" %}
|
||||
"""
|
||||
if value in (None, ''):
|
||||
return ''
|
||||
value = value.astimezone(get_current_timezone())
|
||||
attr_fmt = kwargs["attr_fmt"] if kwargs else None
|
||||
|
||||
try:
|
||||
if not attr_fmt:
|
||||
date_html = value.isoformat()
|
||||
else:
|
||||
date_html = date_fast(value, attr_fmt)
|
||||
|
||||
if dt_format == "format_expires":
|
||||
date_human = LazyExpiresDate(value)
|
||||
else:
|
||||
date_human = date_fast(value, dt_format)
|
||||
return format_html("<time datetime='{}'>{}</time>", date_html, date_human)
|
||||
except AttributeError:
|
||||
return ''
|
||||
@@ -32,7 +32,11 @@ from pretix.base.models import ItemVariation
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.signals import timeline_events
|
||||
|
||||
TimelineEvent = namedtuple('TimelineEvent', ('event', 'subevent', 'datetime', 'description', 'edit_url'))
|
||||
TimelineEvent = namedtuple(
|
||||
'TimelineEvent',
|
||||
('event', 'subevent', 'datetime', 'description', 'edit_url', 'edit_permission'),
|
||||
defaults=(None, None, None, None, None, 'event.settings.general:write')
|
||||
)
|
||||
|
||||
|
||||
def timeline_for_event(event, subevent=None):
|
||||
@@ -46,6 +50,7 @@ def timeline_for_event(event, subevent=None):
|
||||
'subevent': subevent.pk
|
||||
}
|
||||
)
|
||||
ev_edit_permission = 'event.subevents:write'
|
||||
else:
|
||||
ev_edit_url = reverse(
|
||||
'control:event.settings', kwargs={
|
||||
@@ -53,12 +58,14 @@ def timeline_for_event(event, subevent=None):
|
||||
'organizer': event.organizer.slug
|
||||
}
|
||||
)
|
||||
ev_edit_permission = 'event.settings.general:write'
|
||||
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=ev.date_from,
|
||||
description=pgettext_lazy('timeline', 'Your event starts'),
|
||||
edit_url=ev_edit_url + '#id_date_from_0'
|
||||
edit_url=ev_edit_url + '#id_date_from_0',
|
||||
edit_permission=ev_edit_permission,
|
||||
))
|
||||
|
||||
if ev.date_to:
|
||||
@@ -66,7 +73,8 @@ def timeline_for_event(event, subevent=None):
|
||||
event=event, subevent=subevent,
|
||||
datetime=ev.date_to,
|
||||
description=pgettext_lazy('timeline', 'Your event ends'),
|
||||
edit_url=ev_edit_url + '#id_date_to_0'
|
||||
edit_url=ev_edit_url + '#id_date_to_0',
|
||||
edit_permission=ev_edit_permission,
|
||||
))
|
||||
|
||||
if ev.date_admission:
|
||||
@@ -74,7 +82,8 @@ def timeline_for_event(event, subevent=None):
|
||||
event=event, subevent=subevent,
|
||||
datetime=ev.date_admission,
|
||||
description=pgettext_lazy('timeline', 'Admissions for your event start'),
|
||||
edit_url=ev_edit_url + '#id_date_admission_0'
|
||||
edit_url=ev_edit_url + '#id_date_admission_0',
|
||||
edit_permission=ev_edit_permission,
|
||||
))
|
||||
|
||||
if ev.presale_start:
|
||||
@@ -82,7 +91,8 @@ def timeline_for_event(event, subevent=None):
|
||||
event=event, subevent=subevent,
|
||||
datetime=ev.presale_start,
|
||||
description=pgettext_lazy('timeline', 'Start of ticket sales'),
|
||||
edit_url=ev_edit_url + '#id_presale_start_0'
|
||||
edit_url=ev_edit_url + '#id_presale_start_0',
|
||||
edit_permission=ev_edit_permission,
|
||||
))
|
||||
|
||||
tl.append(TimelineEvent(
|
||||
@@ -97,7 +107,8 @@ def timeline_for_event(event, subevent=None):
|
||||
) if not ev.presale_end else (
|
||||
pgettext_lazy('timeline', 'End of ticket sales')
|
||||
),
|
||||
edit_url=ev_edit_url + '#id_presale_end_0'
|
||||
edit_url=ev_edit_url + '#id_presale_end_0',
|
||||
edit_permission=ev_edit_permission,
|
||||
))
|
||||
|
||||
rd = event.settings.get('last_order_modification_date', as_type=RelativeDateWrapper)
|
||||
@@ -106,7 +117,8 @@ def timeline_for_event(event, subevent=None):
|
||||
event=event, subevent=subevent,
|
||||
datetime=rd.datetime(ev),
|
||||
description=pgettext_lazy('timeline', 'Customers can no longer modify their order information'),
|
||||
edit_url=ev_edit_url + '#id_settings-last_order_modification_date_0_0'
|
||||
edit_url=ev_edit_url + '#id_settings-last_order_modification_date_0_0',
|
||||
edit_permission='event.settings.general:write',
|
||||
))
|
||||
|
||||
rd = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
||||
@@ -122,7 +134,8 @@ def timeline_for_event(event, subevent=None):
|
||||
edit_url=reverse('control:event.settings.payment', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug
|
||||
})
|
||||
}),
|
||||
edit_permission='event.settings.payment:write',
|
||||
))
|
||||
|
||||
rd = event.settings.get('ticket_download_date', as_type=RelativeDateWrapper)
|
||||
@@ -134,7 +147,8 @@ def timeline_for_event(event, subevent=None):
|
||||
edit_url=reverse('control:event.settings.tickets', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug
|
||||
})
|
||||
}),
|
||||
edit_permission='event.settings.general:write',
|
||||
))
|
||||
|
||||
rd = event.settings.get('cancel_allow_user_until', as_type=RelativeDateWrapper)
|
||||
@@ -146,7 +160,8 @@ def timeline_for_event(event, subevent=None):
|
||||
edit_url=reverse('control:event.settings.cancel', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug
|
||||
})
|
||||
}),
|
||||
edit_permission='event.settings.general:write',
|
||||
))
|
||||
|
||||
rd = event.settings.get('cancel_allow_user_paid_until', as_type=RelativeDateWrapper)
|
||||
@@ -158,7 +173,8 @@ def timeline_for_event(event, subevent=None):
|
||||
edit_url=reverse('control:event.settings.cancel', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug
|
||||
})
|
||||
}),
|
||||
edit_permission='event.settings.general:write',
|
||||
))
|
||||
|
||||
rd = event.settings.get('change_allow_user_until', as_type=RelativeDateWrapper)
|
||||
@@ -170,7 +186,8 @@ def timeline_for_event(event, subevent=None):
|
||||
edit_url=reverse('control:event.settings.cancel', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug
|
||||
})
|
||||
}),
|
||||
edit_permission='event.settings.general:write',
|
||||
))
|
||||
|
||||
rd = event.settings.get('waiting_list_auto_disable', as_type=RelativeDateWrapper)
|
||||
@@ -182,7 +199,8 @@ def timeline_for_event(event, subevent=None):
|
||||
edit_url=reverse('control:event.settings', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug
|
||||
}) + '#waiting-list-open'
|
||||
}) + '#waiting-list-open',
|
||||
edit_permission='event.settings.general:write',
|
||||
))
|
||||
|
||||
if not event.has_subevents:
|
||||
@@ -196,7 +214,8 @@ def timeline_for_event(event, subevent=None):
|
||||
edit_url=reverse('control:event.settings.mail', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug
|
||||
})
|
||||
}),
|
||||
edit_permission='event.settings.general:write',
|
||||
))
|
||||
|
||||
if subevent:
|
||||
@@ -210,7 +229,8 @@ def timeline_for_event(event, subevent=None):
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'subevent': subevent.pk,
|
||||
})
|
||||
}),
|
||||
edit_permission='event.subevents:write',
|
||||
))
|
||||
if sei.available_until:
|
||||
tl.append(TimelineEvent(
|
||||
@@ -221,7 +241,8 @@ def timeline_for_event(event, subevent=None):
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'subevent': subevent.pk,
|
||||
})
|
||||
}),
|
||||
edit_permission='event.subevents:write',
|
||||
))
|
||||
for sei in subevent.var_overrides.values():
|
||||
if sei.available_from:
|
||||
@@ -234,7 +255,8 @@ def timeline_for_event(event, subevent=None):
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'subevent': subevent.pk,
|
||||
})
|
||||
}),
|
||||
edit_permission='event.subevents:write',
|
||||
))
|
||||
if sei.available_until:
|
||||
tl.append(TimelineEvent(
|
||||
@@ -246,7 +268,8 @@ def timeline_for_event(event, subevent=None):
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'subevent': subevent.pk,
|
||||
})
|
||||
}),
|
||||
edit_permission='event.subevents:write',
|
||||
))
|
||||
|
||||
for d in event.discounts.filter(Q(available_from__isnull=False) | Q(available_until__isnull=False)):
|
||||
@@ -259,7 +282,8 @@ def timeline_for_event(event, subevent=None):
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'discount': d.pk,
|
||||
})
|
||||
}),
|
||||
edit_permission='event.items:write',
|
||||
))
|
||||
if d.available_until:
|
||||
tl.append(TimelineEvent(
|
||||
@@ -270,7 +294,8 @@ def timeline_for_event(event, subevent=None):
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'discount': d.pk,
|
||||
})
|
||||
}),
|
||||
edit_permission='event.items:write',
|
||||
))
|
||||
|
||||
for p in event.items.filter(Q(available_from__isnull=False) | Q(available_until__isnull=False)):
|
||||
@@ -283,7 +308,8 @@ def timeline_for_event(event, subevent=None):
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'item': p.pk,
|
||||
}) + '#id_available_from_0'
|
||||
}) + '#id_available_from_0',
|
||||
edit_permission='event.items:write',
|
||||
))
|
||||
if p.available_until:
|
||||
tl.append(TimelineEvent(
|
||||
@@ -294,7 +320,8 @@ def timeline_for_event(event, subevent=None):
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'item': p.pk,
|
||||
}) + '#id_available_until_0'
|
||||
}) + '#id_available_until_0',
|
||||
edit_permission='event.items:write',
|
||||
))
|
||||
|
||||
for v in ItemVariation.objects.filter(
|
||||
@@ -313,7 +340,8 @@ def timeline_for_event(event, subevent=None):
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'item': v.item.pk,
|
||||
}) + '#tab-0-3-open'
|
||||
}) + '#tab-0-3-open',
|
||||
edit_permission='event.items:write',
|
||||
))
|
||||
if v.available_until:
|
||||
tl.append(TimelineEvent(
|
||||
@@ -327,7 +355,8 @@ def timeline_for_event(event, subevent=None):
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'item': v.item.pk,
|
||||
}) + '#tab-0-3-open'
|
||||
}) + '#tab-0-3-open',
|
||||
edit_permission='event.items:write',
|
||||
))
|
||||
|
||||
pprovs = event.get_payment_providers()
|
||||
@@ -357,7 +386,8 @@ def timeline_for_event(event, subevent=None):
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'provider': pprov.identifier,
|
||||
})
|
||||
}),
|
||||
edit_permission='event.settings.payment:write',
|
||||
))
|
||||
availability_date = pprov.settings.get('_availability_date', as_type=RelativeDateWrapper)
|
||||
if availability_date:
|
||||
@@ -375,7 +405,8 @@ def timeline_for_event(event, subevent=None):
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'provider': pprov.identifier,
|
||||
})
|
||||
}),
|
||||
edit_permission='event.settings.payment:write',
|
||||
))
|
||||
|
||||
for recv, resp in timeline_events.send(sender=event, subevent=subevent):
|
||||
|
||||
@@ -36,9 +36,8 @@ class DownloadView(TemplateView):
|
||||
def object(self) -> CachedFile:
|
||||
try:
|
||||
o = get_object_or_404(CachedFile, id=self.kwargs['id'], web_download=True)
|
||||
if o.session_key:
|
||||
if o.session_key != self.request.session.session_key:
|
||||
raise Http404()
|
||||
if not o.allowed_for_session(self.request):
|
||||
raise Http404()
|
||||
return o
|
||||
except (ValueError, ValidationError): # Invalid URLs
|
||||
raise Http404()
|
||||
|
||||
@@ -38,7 +38,8 @@ from pretix.base.settings import (
|
||||
|
||||
VAT_ID_LABELS = {
|
||||
# VAT ID is a EU concept and Switzerland has a distinct, but differently-named concept
|
||||
"CH": pgettext_lazy("tax_id_swiss", "UID"), # Translators: Only translate to French (IDE) and Italien (IDI), otherwise keep the same
|
||||
# Translators: Only translate to French (IDE) and Italien (IDI), otherwise keep the same
|
||||
"CH": pgettext_lazy("tax_id_swiss", "UID"),
|
||||
|
||||
# Awareness around VAT IDs differes by EU country. For example, in Germany the VAT ID is assigned
|
||||
# separately to each company and only used in cross-country transactions. Therefore, it makes sense
|
||||
@@ -46,10 +47,15 @@ VAT_ID_LABELS = {
|
||||
# In contrast, in Italy the EU-compatible VAT ID is not separately assigned, but is just "IT" + the national tax
|
||||
# number (Partita IVA) and also used on domestic transactions. So someone who never purchased something international
|
||||
# for their company, might still know the value, if we call it the right way and not just "VAT ID".
|
||||
"IT": pgettext_lazy("tax_id_italy", "VAT ID / P.IVA"), # Translators: Translate to only "P.IVA" in Italian, keep second part as-is in other languages
|
||||
"GR": pgettext_lazy("tax_id_greece", "VAT ID / TIN"), # Translators: Translate to only "ΑΦΜ" in Greek
|
||||
"ES": pgettext_lazy("tax_id_spain", "VAT ID / NIF"), # Translators: Translate to only "NIF" in Spanish
|
||||
"PT": pgettext_lazy("tax_id_portugal", "VAT ID / NIF"), # Translators: Translate to only "NIF" in Portuguese
|
||||
|
||||
# Translators: Translate to only "P.IVA" in Italian, keep second part as-is in other languages
|
||||
"IT": pgettext_lazy("tax_id_italy", "VAT ID / P.IVA"),
|
||||
# Translators: Translate to only "ΑΦΜ" in Greek
|
||||
"GR": pgettext_lazy("tax_id_greece", "VAT ID / TIN"),
|
||||
# Translators: Translate to only "NIF" in Spanish
|
||||
"ES": pgettext_lazy("tax_id_spain", "VAT ID / NIF"),
|
||||
# Translators: Translate to only "NIF" in Portuguese
|
||||
"PT": pgettext_lazy("tax_id_portugal", "VAT ID / NIF"),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ def _default_context(request):
|
||||
complain_testmode_orders = request.event.orders.filter(testmode=True).exists()
|
||||
request.event.cache.set('complain_testmode_orders', complain_testmode_orders, 30)
|
||||
ctx['complain_testmode_orders'] = complain_testmode_orders and request.user.has_event_permission(
|
||||
request.organizer, request.event, 'can_view_orders', request=request
|
||||
request.organizer, request.event, 'event.orders:read', request=request
|
||||
)
|
||||
else:
|
||||
ctx['complain_testmode_orders'] = False
|
||||
|
||||
@@ -45,7 +45,7 @@ from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
|
||||
from django.db.models import Prefetch, Q, prefetch_related_objects
|
||||
from django.forms import formset_factory, inlineformset_factory
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.functional import cached_property, lazy
|
||||
from django.utils.html import escape, format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import get_current_timezone_name
|
||||
@@ -53,7 +53,7 @@ from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
|
||||
from django_countries.fields import LazyTypedChoiceField
|
||||
from django_scopes.forms import SafeModelMultipleChoiceField
|
||||
from i18nfield.forms import (
|
||||
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextInput,
|
||||
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput,
|
||||
)
|
||||
from pytz import common_timezones
|
||||
|
||||
@@ -62,6 +62,7 @@ from pretix.base.forms import (
|
||||
)
|
||||
from pretix.base.models import Event, Organizer, TaxRule, Team
|
||||
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
|
||||
from pretix.base.models.organizer import TeamQuerySet
|
||||
from pretix.base.models.tax import TAX_CODE_LISTS
|
||||
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
||||
from pretix.base.services.placeholders import FormPlaceholderMixin
|
||||
@@ -104,7 +105,7 @@ class EventWizardFoundationForm(forms.Form):
|
||||
qs = Organizer.objects.all()
|
||||
if not self.user.has_active_staff_session(self.session.session_key):
|
||||
qs = qs.filter(
|
||||
id__in=self.user.teams.filter(can_create_events=True).values_list('organizer', flat=True)
|
||||
id__in=self.user.teams.filter(TeamQuerySet.organizer_permission_q("organizer.events:create")).values_list('organizer', flat=True)
|
||||
)
|
||||
self.fields['organizer'] = forms.ModelChoiceField(
|
||||
label=_("Organizer"),
|
||||
@@ -262,8 +263,12 @@ class EventWizardBasicsForm(I18nModelForm):
|
||||
@staticmethod
|
||||
def has_control_rights(user, organizer, session):
|
||||
return user.teams.filter(
|
||||
organizer=organizer, all_events=True, can_change_event_settings=True, can_change_items=True,
|
||||
can_change_orders=True, can_change_vouchers=True
|
||||
TeamQuerySet.event_permission_q("event.items:write"),
|
||||
TeamQuerySet.event_permission_q("event.orders:write"),
|
||||
TeamQuerySet.event_permission_q("event.vouchers:write"),
|
||||
TeamQuerySet.event_permission_q("event.settings.general:write"),
|
||||
organizer=organizer,
|
||||
all_events=True,
|
||||
).exists() or user.has_active_staff_session(session.session_key)
|
||||
|
||||
|
||||
@@ -294,9 +299,14 @@ class EventWizardCopyForm(forms.Form):
|
||||
return Event.objects.all()
|
||||
return Event.objects.filter(
|
||||
Q(organizer_id__in=user.teams.filter(
|
||||
all_events=True, can_change_event_settings=True, can_change_items=True
|
||||
# TODO: review these!
|
||||
# Restrict cross-organizer copying further than same-organizer copying?
|
||||
TeamQuerySet.event_permission_q("event.settings.general:write"),
|
||||
TeamQuerySet.event_permission_q("event.items:write"),
|
||||
all_events=True,
|
||||
).values_list('organizer', flat=True)) | Q(id__in=user.teams.filter(
|
||||
can_change_event_settings=True, can_change_items=True
|
||||
TeamQuerySet.event_permission_q("event.settings.general:write"),
|
||||
TeamQuerySet.event_permission_q("event.items:write"),
|
||||
).values_list('limit_events__id', flat=True))
|
||||
)
|
||||
|
||||
@@ -1311,9 +1321,17 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
|
||||
mail_text_order_invoice = I18nFormField(
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
widget=I18nMarkdownTextarea,
|
||||
help_text=_("This will only be used if the invoice is sent to a different email address or at a different time "
|
||||
"than the order confirmation."),
|
||||
widget=I18nTextarea, # no Markdown supported
|
||||
help_text=lazy(
|
||||
lambda: str(_(
|
||||
"This will only be used if the invoice is sent to a different email address or at a different time "
|
||||
"than the order confirmation."
|
||||
)) + " " + str(_(
|
||||
"Formatting is not supported, as some accounting departments process mail automatically and do not "
|
||||
"handle formatted emails properly."
|
||||
)),
|
||||
str
|
||||
)()
|
||||
)
|
||||
mail_subject_download_reminder = I18nFormField(
|
||||
label=_("Subject sent to order contact address"),
|
||||
@@ -1481,6 +1499,9 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
|
||||
'mail_subject_resend_all_links': ['event', 'orders'],
|
||||
'mail_attach_ical_description': ['event', 'event_or_subevent'],
|
||||
}
|
||||
plain_rendering = {
|
||||
'mail_text_order_invoice',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = event = kwargs.get('obj')
|
||||
@@ -1499,7 +1520,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
|
||||
self.event.meta_values_cached = self.event.meta_values.select_related('property').all()
|
||||
|
||||
for k, v in self.base_context.items():
|
||||
self._set_field_placeholders(k, v, rich=k.startswith('mail_text_'))
|
||||
self._set_field_placeholders(k, v, rich=k.startswith('mail_text_') and k not in self.plain_rendering)
|
||||
|
||||
for k, v in list(self.fields.items()):
|
||||
if k.endswith('_attendee') and not event.settings.attendee_emails_asked:
|
||||
|
||||
@@ -61,6 +61,10 @@ from pretix.base.models import (
|
||||
SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher,
|
||||
)
|
||||
from pretix.base.signals import register_payment_providers
|
||||
from pretix.base.timeframes import (
|
||||
DateFrameField,
|
||||
resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
|
||||
)
|
||||
from pretix.control.forms import SplitDateTimeField
|
||||
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
|
||||
from pretix.control.signals import order_search_filter_q
|
||||
@@ -1106,7 +1110,7 @@ class OrderPaymentSearchFilterForm(forms.Form):
|
||||
self.fields['organizer'].queryset = Organizer.objects.filter(
|
||||
pk__in=self.request.user.teams.values_list('organizer', flat=True)
|
||||
)
|
||||
self.fields['event'].queryset = self.request.user.get_events_with_permission('can_view_orders')
|
||||
self.fields['event'].queryset = self.request.user.get_events_with_permission('event.orders:read')
|
||||
|
||||
self.fields['provider'].choices += get_all_payment_providers()
|
||||
|
||||
@@ -1219,6 +1223,129 @@ class OrderPaymentSearchFilterForm(forms.Form):
|
||||
return qs
|
||||
|
||||
|
||||
class QuestionAnswerFilterForm(forms.Form):
|
||||
STATUS_VARIANTS = [
|
||||
("", _("All orders")),
|
||||
(Order.STATUS_PAID, _("Paid")),
|
||||
(Order.STATUS_PAID + 'v', _("Paid or confirmed")),
|
||||
(Order.STATUS_PENDING, _("Pending")),
|
||||
(Order.STATUS_PENDING + Order.STATUS_PAID, _("Pending or paid")),
|
||||
("o", _("Pending (overdue)")),
|
||||
(Order.STATUS_EXPIRED, _("Expired")),
|
||||
(Order.STATUS_PENDING + Order.STATUS_EXPIRED, _("Pending or expired")),
|
||||
(Order.STATUS_CANCELED, _("Canceled"))
|
||||
]
|
||||
|
||||
status = forms.ChoiceField(
|
||||
choices=STATUS_VARIANTS,
|
||||
required=False,
|
||||
label=_("Order status"),
|
||||
)
|
||||
item = forms.ChoiceField(
|
||||
choices=[],
|
||||
required=False,
|
||||
label=_("Products"),
|
||||
)
|
||||
subevent = forms.ModelChoiceField(
|
||||
queryset=SubEvent.objects.none(),
|
||||
required=False,
|
||||
empty_label=pgettext_lazy('subevent', 'All dates'),
|
||||
label=pgettext_lazy("subevent", "Date"),
|
||||
)
|
||||
date_range = DateFrameField(
|
||||
required=False,
|
||||
include_future_frames=True,
|
||||
label=_('Event date'),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.initial['status'] = Order.STATUS_PENDING + Order.STATUS_PAID
|
||||
|
||||
choices = [('', _('All products'))]
|
||||
for i in self.event.items.prefetch_related('variations').all():
|
||||
variations = list(i.variations.all())
|
||||
if variations:
|
||||
choices.append((str(i.pk), _('{product} – Any variation').format(product=str(i))))
|
||||
for v in variations:
|
||||
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (str(i), v.value)))
|
||||
else:
|
||||
choices.append((str(i.pk), str(i)))
|
||||
self.fields['item'].choices = choices
|
||||
|
||||
if self.event.has_subevents:
|
||||
self.fields["subevent"].queryset = self.event.subevents.all()
|
||||
self.fields['subevent'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'event',
|
||||
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
|
||||
'event': self.event.slug,
|
||||
'organizer': self.event.organizer.slug,
|
||||
}),
|
||||
'data-placeholder': pgettext_lazy('subevent', 'All dates')
|
||||
}
|
||||
)
|
||||
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
|
||||
else:
|
||||
del self.fields['subevent']
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
subevent = cleaned_data.get('subevent')
|
||||
date_range = cleaned_data.get('date_range')
|
||||
|
||||
if subevent is not None and date_range is not None:
|
||||
d_start, d_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), date_range, self.event.timezone)
|
||||
if (
|
||||
(d_start and not (d_start <= subevent.date_from)) or
|
||||
(d_end and not (subevent.date_from < d_end))
|
||||
):
|
||||
self.add_error('subevent', pgettext_lazy('subevent', "Date doesn't start in selected date range."))
|
||||
return cleaned_data
|
||||
|
||||
def filter_qs(self, opqs):
|
||||
fdata = self.cleaned_data
|
||||
|
||||
subevent = fdata.get('subevent', None)
|
||||
date_range = fdata.get('date_range', None)
|
||||
|
||||
if subevent is not None:
|
||||
opqs = opqs.filter(subevent=subevent)
|
||||
|
||||
if date_range is not None:
|
||||
d_start, d_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), date_range, self.event.timezone)
|
||||
opqs = opqs.filter(
|
||||
subevent__date_from__gte=d_start,
|
||||
subevent__date_from__lt=d_end
|
||||
)
|
||||
|
||||
s = fdata.get("status", Order.STATUS_PENDING + Order.STATUS_PAID)
|
||||
if s != "":
|
||||
if s == Order.STATUS_PENDING:
|
||||
opqs = opqs.filter(order__status=Order.STATUS_PENDING,
|
||||
order__expires__lt=now().replace(hour=0, minute=0, second=0))
|
||||
elif s == Order.STATUS_PENDING + Order.STATUS_PAID:
|
||||
opqs = opqs.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID])
|
||||
elif s == Order.STATUS_PAID + 'v':
|
||||
opqs = opqs.filter(
|
||||
Q(order__status=Order.STATUS_PAID) |
|
||||
Q(order__status=Order.STATUS_PENDING, order__valid_if_pending=True)
|
||||
)
|
||||
elif s == Order.STATUS_PENDING + Order.STATUS_EXPIRED:
|
||||
opqs = opqs.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_EXPIRED])
|
||||
else:
|
||||
opqs = opqs.filter(order__status=s)
|
||||
|
||||
if s not in (Order.STATUS_CANCELED, ""):
|
||||
opqs = opqs.filter(canceled=False)
|
||||
if fdata.get("item", "") != "":
|
||||
i = fdata.get("item", "")
|
||||
opqs = opqs.filter(item_id__in=(i,))
|
||||
|
||||
return opqs
|
||||
|
||||
|
||||
class SubEventFilterForm(FilterForm):
|
||||
orders = {
|
||||
'date_from': 'date_from',
|
||||
|
||||
@@ -75,7 +75,10 @@ from pretix.base.models import (
|
||||
ReusableMedium, SalesChannel, Team,
|
||||
)
|
||||
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
|
||||
from pretix.base.models.organizer import OrganizerFooterLink
|
||||
from pretix.base.models.organizer import OrganizerFooterLink, TeamQuerySet
|
||||
from pretix.base.permissions import (
|
||||
get_all_event_permissions, get_all_organizer_permissions,
|
||||
)
|
||||
from pretix.base.settings import (
|
||||
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_organizer_settings,
|
||||
)
|
||||
@@ -297,7 +300,34 @@ class MembershipTypeForm(I18nModelForm):
|
||||
fields = ['name', 'transferable', 'allow_parallel_usage', 'max_usages']
|
||||
|
||||
|
||||
class PermissionMultipleChoiceField(forms.MultipleChoiceField):
|
||||
def to_python(self, value):
|
||||
return {
|
||||
k: True for k in super().to_python(value) if k
|
||||
}
|
||||
|
||||
def prepare_value(self, value):
|
||||
if isinstance(value, dict):
|
||||
return [k for k, v in value.items() if v is True]
|
||||
return super().prepare_value(value)
|
||||
|
||||
|
||||
class TeamForm(forms.ModelForm):
|
||||
def _make_label(self, p):
|
||||
source = '{}'
|
||||
params = [p.label]
|
||||
|
||||
if p.plugin_name:
|
||||
source = '<span class="fa fa-puzzle-piece text-muted" data-toggle="tooltip" title="{}"></span> ' + source
|
||||
params.insert(0, _("Provided by a plugin"))
|
||||
|
||||
if p.help_text:
|
||||
source += ' <span class="fa fa-info-circle text-muted" data-toggle="tooltip" title="{}"></span>'
|
||||
params.append(p.help_text)
|
||||
|
||||
source += ' (<code>{}</code>)'
|
||||
params.append(p.name)
|
||||
return format_html(source, *params)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
organizer = kwargs.pop('organizer')
|
||||
@@ -305,16 +335,34 @@ class TeamForm(forms.ModelForm):
|
||||
self.fields['limit_events'].queryset = organizer.events.all().order_by(
|
||||
'-has_subevents', '-date_from'
|
||||
)
|
||||
self.fields['limit_event_permissions'] = PermissionMultipleChoiceField(
|
||||
label=self.fields['limit_event_permissions'].label,
|
||||
choices=[
|
||||
(p.name, self._make_label(p)) for p in get_all_event_permissions().values()
|
||||
],
|
||||
widget=forms.CheckboxSelectMultiple(attrs={
|
||||
'data-inverse-dependency': '#id_all_event_permissions',
|
||||
'class': 'scrolling-multiple-choice scrolling-multiple-choice-large',
|
||||
}),
|
||||
required=False,
|
||||
)
|
||||
self.fields['limit_organizer_permissions'] = PermissionMultipleChoiceField(
|
||||
label=self.fields['limit_organizer_permissions'].label,
|
||||
choices=[
|
||||
(p.name, self._make_label(p)) for p in get_all_organizer_permissions().values()
|
||||
],
|
||||
widget=forms.CheckboxSelectMultiple(attrs={
|
||||
'data-inverse-dependency': '#id_all_organizer_permissions',
|
||||
'class': 'scrolling-multiple-choice scrolling-multiple-choice-large',
|
||||
}),
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = ['name', 'require_2fa', 'all_events', 'limit_events', 'can_create_events',
|
||||
'can_change_teams', 'can_change_organizer_settings',
|
||||
'can_manage_gift_cards', 'can_manage_customers',
|
||||
'can_manage_reusable_media',
|
||||
'can_change_event_settings', 'can_change_items',
|
||||
'can_view_orders', 'can_change_orders', 'can_checkin_orders',
|
||||
'can_view_vouchers', 'can_change_vouchers']
|
||||
fields = ['name', 'require_2fa', 'all_events', 'limit_events',
|
||||
'all_event_permissions', 'limit_event_permissions',
|
||||
'all_organizer_permissions', 'limit_organizer_permissions']
|
||||
widgets = {
|
||||
'limit_events': forms.CheckboxSelectMultiple(attrs={
|
||||
'data-inverse-dependency': '#id_all_events',
|
||||
@@ -327,9 +375,10 @@ class TeamForm(forms.ModelForm):
|
||||
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
if self.instance.pk and not data['can_change_teams']:
|
||||
if self.instance.pk and not data['all_organizer_permissions'] and 'organizer.teams:write' not in data.get('limit_organizer_permissions', []):
|
||||
if not self.instance.organizer.teams.exclude(pk=self.instance.pk).filter(
|
||||
can_change_teams=True, members__isnull=False
|
||||
TeamQuerySet.organizer_permission_q("organizer.teams:write"),
|
||||
members__isnull=False
|
||||
).exists():
|
||||
raise ValidationError(_('The changes could not be saved because there would be no remaining team with '
|
||||
'the permission to change teams and permissions.'))
|
||||
|
||||
@@ -43,24 +43,29 @@ def get_event_navigation(request: HttpRequest):
|
||||
'icon': 'dashboard',
|
||||
}
|
||||
]
|
||||
if 'can_change_event_settings' in request.eventpermset:
|
||||
event_settings = [
|
||||
{
|
||||
'label': _('General'),
|
||||
'url': reverse('control:event.settings', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': url.url_name == 'event.settings',
|
||||
},
|
||||
{
|
||||
'label': _('Payment'),
|
||||
'url': reverse('control:event.settings.payment', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': url.url_name in ('event.settings.payment', 'event.settings.payment.provider'),
|
||||
},
|
||||
event_settings = []
|
||||
if "event.settings.general:write" in request.eventpermset:
|
||||
event_settings.append({
|
||||
'label': _('General'),
|
||||
'url': reverse('control:event.settings', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': url.url_name == 'event.settings',
|
||||
})
|
||||
|
||||
if "event.settings.payment:write" in request.eventpermset or "event.settings.general:write" in request.eventpermset:
|
||||
event_settings.append({
|
||||
'label': _('Payment'),
|
||||
'url': reverse('control:event.settings.payment', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': url.url_name in ('event.settings.payment', 'event.settings.payment.provider'),
|
||||
})
|
||||
|
||||
if "event.settings.general:write" in request.eventpermset:
|
||||
event_settings += [
|
||||
{
|
||||
'label': _('Plugins'),
|
||||
'url': reverse('control:event.settings.plugins', kwargs={
|
||||
@@ -84,23 +89,31 @@ def get_event_navigation(request: HttpRequest):
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': url.url_name == 'event.settings.mail',
|
||||
},
|
||||
{
|
||||
'label': _('Taxes'),
|
||||
'url': reverse('control:event.settings.tax', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': url.url_name.startswith('event.settings.tax'),
|
||||
},
|
||||
{
|
||||
'label': _('Invoicing'),
|
||||
'url': reverse('control:event.settings.invoice', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': url.url_name == 'event.settings.invoice',
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
if "event.settings.tax:write" in request.eventpermset or "event.settings.general:write" in request.eventpermset:
|
||||
event_settings.append({
|
||||
'label': _('Taxes'),
|
||||
'url': reverse('control:event.settings.tax', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': url.url_name.startswith('event.settings.tax'),
|
||||
})
|
||||
|
||||
if "event.settings.invoicing:write" in request.eventpermset or "event.settings.general:write" in request.eventpermset:
|
||||
event_settings.append({
|
||||
'label': _('Invoicing'),
|
||||
'url': reverse('control:event.settings.invoice', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': url.url_name == 'event.settings.invoice',
|
||||
})
|
||||
|
||||
if "event.settings.general:write" in request.eventpermset:
|
||||
event_settings += [
|
||||
{
|
||||
'label': pgettext_lazy('action', 'Cancellation'),
|
||||
'url': reverse('control:event.settings.cancel', kwargs={
|
||||
@@ -118,88 +131,87 @@ def get_event_navigation(request: HttpRequest):
|
||||
'active': url.url_name == 'event.settings.widget',
|
||||
},
|
||||
]
|
||||
|
||||
# It would be better to allow plugins to handle the permission themselves, but for backwards compatibility
|
||||
# we need to have it in the "if" statement
|
||||
event_settings += sorted(
|
||||
sum((list(a[1]) for a in nav_event_settings.send(request.event, request=request)), []),
|
||||
key=lambda r: r['label']
|
||||
)
|
||||
if event_settings:
|
||||
nav.append({
|
||||
'label': _('Settings'),
|
||||
'url': reverse('control:event.settings', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'url': event_settings[0]["url"],
|
||||
'active': False,
|
||||
'icon': 'wrench',
|
||||
'children': event_settings
|
||||
})
|
||||
|
||||
if 'can_change_items' in request.eventpermset:
|
||||
nav.append({
|
||||
'label': _('Products'),
|
||||
'url': reverse('control:event.items', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': False,
|
||||
'icon': 'ticket',
|
||||
'children': [
|
||||
{
|
||||
'label': _('Products'),
|
||||
'url': reverse('control:event.items', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': url.url_name in (
|
||||
'event.item', 'event.items.add', 'event.items') or "event.item." in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Quotas'),
|
||||
'url': reverse('control:event.items.quotas', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.items.quota' in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Categories'),
|
||||
'url': reverse('control:event.items.categories', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.items.categories' in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Questions'),
|
||||
'url': reverse('control:event.items.questions', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.items.questions' in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Discounts'),
|
||||
'url': reverse('control:event.items.discounts', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.items.discounts' in url.url_name,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
if 'can_change_event_settings' in request.eventpermset:
|
||||
if request.event.has_subevents:
|
||||
nav.append({
|
||||
'label': pgettext_lazy('subevent', 'Dates'),
|
||||
'url': reverse('control:event.subevents', kwargs={
|
||||
nav.append({
|
||||
'label': _('Products'),
|
||||
'url': reverse('control:event.items', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': False,
|
||||
'icon': 'ticket',
|
||||
'children': [
|
||||
{
|
||||
'label': _('Products'),
|
||||
'url': reverse('control:event.items', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': ('event.subevent' in url.url_name),
|
||||
'icon': 'calendar',
|
||||
})
|
||||
'active': url.url_name in (
|
||||
'event.item', 'event.items.add', 'event.items') or "event.item." in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Quotas'),
|
||||
'url': reverse('control:event.items.quotas', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.items.quota' in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Categories'),
|
||||
'url': reverse('control:event.items.categories', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.items.categories' in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Questions'),
|
||||
'url': reverse('control:event.items.questions', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.items.questions' in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Discounts'),
|
||||
'url': reverse('control:event.items.discounts', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.items.discounts' in url.url_name,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
if 'can_view_orders' in request.eventpermset:
|
||||
if request.event.has_subevents:
|
||||
nav.append({
|
||||
'label': pgettext_lazy('subevent', 'Dates'),
|
||||
'url': reverse('control:event.subevents', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': ('event.subevent' in url.url_name),
|
||||
'icon': 'calendar',
|
||||
})
|
||||
|
||||
if 'event.orders:read' in request.eventpermset:
|
||||
children = [
|
||||
{
|
||||
'label': _('All orders'),
|
||||
@@ -242,7 +254,7 @@ def get_event_navigation(request: HttpRequest):
|
||||
'active': 'event.orders.waitinglist' in url.url_name,
|
||||
},
|
||||
]
|
||||
if 'can_change_orders' in request.eventpermset:
|
||||
if 'event.orders:write' in request.eventpermset:
|
||||
children.append({
|
||||
'label': _('Import'),
|
||||
'url': reverse('control:event.orders.import', kwargs={
|
||||
@@ -262,7 +274,7 @@ def get_event_navigation(request: HttpRequest):
|
||||
'children': children
|
||||
})
|
||||
|
||||
if 'can_view_vouchers' in request.eventpermset:
|
||||
if 'event.vouchers:read' in request.eventpermset:
|
||||
nav.append({
|
||||
'label': _('Vouchers'),
|
||||
'url': reverse('control:event.vouchers', kwargs={
|
||||
@@ -291,7 +303,7 @@ def get_event_navigation(request: HttpRequest):
|
||||
]
|
||||
})
|
||||
|
||||
if 'can_view_orders' in request.eventpermset:
|
||||
if 'event.orders:read' in request.eventpermset or 'event.settings.general:write' in request.eventpermset:
|
||||
nav.append({
|
||||
'label': pgettext_lazy('navigation', 'Check-in'),
|
||||
'url': reverse('control:event.orders.checkinlists', kwargs={
|
||||
@@ -480,7 +492,7 @@ def get_organizer_navigation(request):
|
||||
'icon': 'calendar',
|
||||
},
|
||||
]
|
||||
if 'can_change_organizer_settings' in request.orgapermset:
|
||||
if 'organizer.settings.general:write' in request.orgapermset:
|
||||
nav.append({
|
||||
'label': _('Settings'),
|
||||
'url': reverse('control:organizer.edit', kwargs={
|
||||
@@ -534,7 +546,7 @@ def get_organizer_navigation(request):
|
||||
]
|
||||
})
|
||||
|
||||
if 'can_change_teams' in request.orgapermset:
|
||||
if 'organizer.teams:write' in request.orgapermset:
|
||||
nav.append({
|
||||
'label': _('Teams'),
|
||||
'url': reverse('control:organizer.teams', kwargs={
|
||||
@@ -544,7 +556,7 @@ def get_organizer_navigation(request):
|
||||
'icon': 'group',
|
||||
})
|
||||
|
||||
if 'can_manage_gift_cards' in request.orgapermset:
|
||||
if 'organizer.giftcards:read' in request.orgapermset or 'organizer.giftcards:write' in request.orgapermset:
|
||||
children = []
|
||||
children.append({
|
||||
'label': _('Gift cards'),
|
||||
@@ -554,7 +566,7 @@ def get_organizer_navigation(request):
|
||||
'active': 'organizer.giftcard' in url.url_name and 'acceptance' not in url.url_name,
|
||||
'children': children,
|
||||
})
|
||||
if 'can_change_organizer_settings' in request.orgapermset:
|
||||
if 'organizer.settings.general:write' in request.orgapermset:
|
||||
children.append(
|
||||
{
|
||||
'label': _('Acceptance'),
|
||||
@@ -575,7 +587,7 @@ def get_organizer_navigation(request):
|
||||
|
||||
if request.organizer.settings.customer_accounts:
|
||||
children = []
|
||||
if 'can_manage_customers' in request.orgapermset:
|
||||
if 'organizer.customers:read' in request.orgapermset or 'organizer.customers:write' in request.orgapermset:
|
||||
children.append(
|
||||
{
|
||||
'label': _('Customers'),
|
||||
@@ -585,7 +597,7 @@ def get_organizer_navigation(request):
|
||||
'active': 'organizer.customer' in url.url_name,
|
||||
}
|
||||
)
|
||||
if 'can_change_organizer_settings' in request.orgapermset:
|
||||
if 'organizer.settings.general:write' in request.orgapermset:
|
||||
children.append(
|
||||
{
|
||||
'label': _('Membership types'),
|
||||
@@ -624,16 +636,17 @@ def get_organizer_navigation(request):
|
||||
})
|
||||
|
||||
if request.organizer.settings.reusable_media_active:
|
||||
nav.append({
|
||||
'label': _('Reusable media'),
|
||||
'url': reverse('control:organizer.reusable_media', kwargs={
|
||||
'organizer': request.organizer.slug
|
||||
}),
|
||||
'icon': 'key',
|
||||
'active': 'organizer.reusable_medi' in url.url_name,
|
||||
})
|
||||
if 'organizer.reusablemedia:read' in request.orgapermset or 'organizer.reusablemedia:write' in request.orgapermset:
|
||||
nav.append({
|
||||
'label': _('Reusable media'),
|
||||
'url': reverse('control:organizer.reusable_media', kwargs={
|
||||
'organizer': request.organizer.slug
|
||||
}),
|
||||
'icon': 'key',
|
||||
'active': 'organizer.reusable_medi' in url.url_name,
|
||||
})
|
||||
|
||||
if 'can_change_organizer_settings' in request.orgapermset:
|
||||
if 'organizer.devices:read' in request.orgapermset or 'organizer.devices:write' in request.orgapermset:
|
||||
nav.append({
|
||||
'label': _('Devices'),
|
||||
'url': reverse('control:organizer.devices', kwargs={
|
||||
@@ -667,7 +680,7 @@ def get_organizer_navigation(request):
|
||||
'icon': 'download',
|
||||
})
|
||||
|
||||
if 'can_change_organizer_settings' in request.orgapermset:
|
||||
if 'organizer.settings.general:write' in request.orgapermset:
|
||||
merge_in(nav, [{
|
||||
'parent': reverse('control:organizer.export', kwargs={
|
||||
'organizer': request.organizer.slug,
|
||||
|
||||
@@ -55,7 +55,7 @@ def event_permission_required(permission):
|
||||
"""
|
||||
if permission == 'can_change_settings':
|
||||
# Legacy support
|
||||
permission = 'can_change_event_settings'
|
||||
permission = 'event.settings.general:write'
|
||||
|
||||
def decorator(function):
|
||||
def wrapper(request, *args, **kw):
|
||||
@@ -92,9 +92,9 @@ def organizer_permission_required(permission):
|
||||
This view decorator rejects all requests with a 403 response which are not from
|
||||
users having the given permission for the event the request is associated with.
|
||||
"""
|
||||
if permission == 'can_change_settings':
|
||||
if permission == 'event.settings.general:write':
|
||||
# Legacy support
|
||||
permission = 'can_change_organizer_settings'
|
||||
permission = 'organizer.settings.general:write'
|
||||
|
||||
def decorator(function):
|
||||
def wrapper(request, *args, **kw):
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}
|
||||
{% if 'can_change_event_settings' in request.eventpermset %}
|
||||
{% if 'event.settings.general:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.orders.checkinlists.edit" event=request.event.slug organizer=request.event.organizer.slug list=checkinlist.pk %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-wrench"></span>
|
||||
@@ -87,7 +87,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{% if "can_change_orders" in request.eventpermset or "can_checkin_orders" in request.eventpermset %}
|
||||
{% if "event.orders:write" in request.eventpermset or "event.orders:checkin" in request.eventpermset %}
|
||||
<label aria-label="{% trans "select all rows for batch-operation" %}"
|
||||
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
|
||||
{% endif %}
|
||||
@@ -132,7 +132,7 @@
|
||||
{% for e in entries %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if "can_change_orders" in request.eventpermset or "can_checkin_orders" in request.eventpermset %}
|
||||
{% if "event.orders:write" in request.eventpermset or "event.orders:checkin" in request.eventpermset %}
|
||||
<input type="checkbox" name="checkin" id="id_checkin" class="" value="{{ e.pk }}"/>
|
||||
{% endif %}
|
||||
</td>
|
||||
@@ -207,7 +207,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<div class="batch-select-actions">
|
||||
{% if "can_change_orders" in request.eventpermset or "can_checkin_orders" in request.eventpermset %}
|
||||
{% if "event.orders:write" in request.eventpermset or "event.orders:checkin" in request.eventpermset %}
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
<span class="fa fa-sign-in" aria-hidden="true"></span>
|
||||
{% trans "Check-In selected attendees" %}
|
||||
@@ -217,7 +217,7 @@
|
||||
{% trans "Check-Out selected attendees" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if "can_change_orders" in request.eventpermset %}
|
||||
{% if "event.orders:write" in request.eventpermset %}
|
||||
<button type="submit" class="btn btn-danger btn-save" name="revert"
|
||||
formaction="{% url "control:event.orders.checkinlists.bulk_revert" event=request.event.slug organizer=request.event.organizer.slug list=checkinlist.pk %}"
|
||||
data-no-asynctask
|
||||
|
||||
@@ -63,27 +63,27 @@
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% if "can_change_event_settings" in request.eventpermset %}
|
||||
{% if "event.settings.general:write" in request.eventpermset %}
|
||||
<a href="{% url "control:event.orders.checkinlists.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new check-in list" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if can_change_organizer_settings %}
|
||||
{% if link_device_settings %}
|
||||
<a href="{% url "control:organizer.devices" organizer=request.organizer.slug %}"
|
||||
class="btn btn-default btn-lg"><i class="fa fa-tablet"></i> {% trans "Connected devices" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
{% if "can_change_event_settings" in request.eventpermset %}
|
||||
{% if "event.settings.general:write" in request.eventpermset %}
|
||||
<a href="{% url "control:event.orders.checkinlists.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new check-in list" %}</a>
|
||||
{% endif %}
|
||||
{% if can_change_organizer_settings %}
|
||||
{% if link_device_settings %}
|
||||
<a href="{% url "control:organizer.devices" organizer=request.organizer.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-tablet"></i> {% trans "Connected devices" %}</a>
|
||||
{% endif %}
|
||||
{% if "can_change_orders" in request.eventpermset %}
|
||||
{% if "event.settings.general:write" in request.eventpermset and "event.orders:write" in request.eventpermset %}
|
||||
<a href="{% url "control:event.orders.checkinlists.reset" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-repeat"></span>
|
||||
@@ -100,7 +100,9 @@
|
||||
<a href="?{% url_replace request 'ordering' '-name' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'name' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th>{% trans "Checked in" %}</th>
|
||||
{% if "event.orders:read" in request.eventpermset %}
|
||||
<th>{% trans "Checked in" %}</th>
|
||||
{% endif %}
|
||||
{% if request.event.has_subevents %}
|
||||
<th>
|
||||
{% trans "Date" context "subevent" %}
|
||||
@@ -119,18 +121,20 @@
|
||||
<strong><a
|
||||
href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}">{{ cl.name }}</a></strong>
|
||||
</td>
|
||||
<td>
|
||||
<div class="quotabox availability">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success progress-bar-{{ cl.percent }}">
|
||||
{% if "event.orders:read" in request.eventpermset %}
|
||||
<td>
|
||||
<div class="quotabox availability">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success progress-bar-{{ cl.percent }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="numbers">
|
||||
{{ cl.checkin_count|default_if_none:"0" }} /
|
||||
{{ cl.position_count|default_if_none:"0" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="numbers">
|
||||
{{ cl.checkin_count|default_if_none:"0" }} /
|
||||
{{ cl.position_count|default_if_none:"0" }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if request.event.has_subevents %}
|
||||
{% if cl.subevent %}
|
||||
<td>
|
||||
@@ -156,16 +160,18 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-eye"></i></a>
|
||||
{% if "can_change_event_settings" in request.eventpermset %}
|
||||
{% if "event.orders:read" in request.eventpermset %}
|
||||
<a href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-eye"></i></a>
|
||||
<a href="{% url "control:event.orders.checkinlists.simulator" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
||||
title="{% trans "Check-in simulator" %}" data-toggle="tooltip"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-flask"></i></a>
|
||||
{% endif %}
|
||||
{% if "event.settings.general:write" in request.eventpermset %}
|
||||
<a href="{% url "control:event.orders.checkinlists.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ cl.id }}"
|
||||
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
|
||||
<span class="fa fa-copy"></span>
|
||||
</a>
|
||||
<a href="{% url "control:event.orders.checkinlists.simulator" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
||||
title="{% trans "Check-in simulator" %}" data-toggle="tooltip"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-flask"></i></a>
|
||||
<a href="{% url "control:event.orders.checkinlists.edit" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-wrench"></i></a>
|
||||
<a href="{% url "control:event.orders.checkinlists.delete" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
{% block inside %}
|
||||
<h1>
|
||||
{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}
|
||||
{% if 'can_change_event_settings' in request.eventpermset %}
|
||||
{% if 'event.settings.general:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.orders.checkinlists.edit" event=request.event.slug organizer=request.event.organizer.slug list=checkinlist.pk %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-wrench"></span>
|
||||
|
||||
@@ -11,18 +11,20 @@
|
||||
<ul class="list-group">
|
||||
{% for identifier, display_name, pending, objects in providers %}
|
||||
<li class="list-group-item">
|
||||
<form action="{% url "control:event.order.sync_job" organizer=event.organizer.slug event=event.slug code=order.code provider=identifier %}" method="post" class="form-inline pull-right">
|
||||
{% csrf_token %}
|
||||
{% if pending %}
|
||||
{% if pending.not_before > now or pending.need_manual_retry %}
|
||||
<button type="submit" name="run_job_now" value="{{ pending.pk }}" class="btn btn-default"><i class="fa fa-refresh"></i> {% trans "Retry now" %}</button>
|
||||
{% if "event.orders:write" in request.eventpermset %}
|
||||
<form action="{% url "control:event.order.sync_job" organizer=event.organizer.slug event=event.slug code=order.code provider=identifier %}" method="post" class="form-inline pull-right">
|
||||
{% csrf_token %}
|
||||
{% if pending %}
|
||||
{% if pending.not_before > now or pending.need_manual_retry %}
|
||||
<button type="submit" name="run_job_now" value="{{ pending.pk }}" class="btn btn-default"><i class="fa fa-refresh"></i> {% trans "Retry now" %}</button>
|
||||
{% endif %}
|
||||
<button type="submit" name="cancel_job" value="{{ pending.pk }}" class="btn btn-danger"><i class="fa fa-times"></i> {% trans "Cancel" %}</button>
|
||||
{% else %}
|
||||
<button type="submit" class="btn btn-default"><i class="fa fa-refresh"></i> {% trans "Sync now" %}</button>
|
||||
<input type="hidden" name="queue_sync" value="true">
|
||||
{% endif %}
|
||||
<button type="submit" name="cancel_job" value="{{ pending.pk }}" class="btn btn-danger"><i class="fa fa-times"></i> {% trans "Cancel" %}</button>
|
||||
{% else %}
|
||||
<button type="submit" class="btn btn-default"><i class="fa fa-refresh"></i> {% trans "Sync now" %}</button>
|
||||
<input type="hidden" name="queue_sync" value="true">
|
||||
{% endif %}
|
||||
</form>
|
||||
</form>
|
||||
{% endif %}
|
||||
<p><b>{{ display_name }}</b></p>
|
||||
{% if pending %}
|
||||
<p>
|
||||
|
||||
@@ -40,12 +40,16 @@
|
||||
this option.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-3">
|
||||
<a href="{% url "control:event.cancel" organizer=request.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-danger btn-block btn-lg">
|
||||
<span class="fa fa-ban"></span>
|
||||
{% trans "Cancel event" %}
|
||||
</a>
|
||||
<div class="col-sm-12 col-md-3 text-center">
|
||||
{% if "event:cancel" in request.eventpermset %}
|
||||
<a href="{% url "control:event.cancel" organizer=request.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-danger btn-block btn-lg">
|
||||
<span class="fa fa-ban"></span>
|
||||
{% trans "Cancel event" %}
|
||||
</a>
|
||||
{% else %}
|
||||
{% trans "No permission" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<span class="{% if e.time < nearly_now %}text-muted{% endif %}">
|
||||
{{ e.entry.description }}
|
||||
</span>
|
||||
{% if e.entry.edit_url %}
|
||||
{% if e.entry.edit_url and e.entry.edit_permission in request.eventpermset %}
|
||||
|
||||
<a href="{{ e.entry.edit_url }}" class="text-muted">
|
||||
<span class="fa fa-edit"></span>
|
||||
|
||||
@@ -155,22 +155,24 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Event logs" %}
|
||||
</h3>
|
||||
</div>
|
||||
<ul class="list-group" id="logs_target">
|
||||
<div class="logs-lazy-loading">
|
||||
<span class="fa fa-cog fa-4x"></span>
|
||||
{% if "event.orders:read" in request.eventpermset or "event.orders:write" in request.eventpermset or "event.settings.general:write" in request.eventpermset or "event.items:write" in request.eventpermset %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Event logs" %}
|
||||
</h3>
|
||||
</div>
|
||||
<ul class="list-group" id="logs_target">
|
||||
<div class="logs-lazy-loading">
|
||||
<span class="fa fa-cog fa-4x"></span>
|
||||
</div>
|
||||
</ul>
|
||||
<div class="panel-footer">
|
||||
<a href="{% url "control:event.log" event=request.event.slug organizer=request.event.organizer.slug %}">
|
||||
{% trans "Show more logs" %}
|
||||
</a>
|
||||
</div>
|
||||
</ul>
|
||||
<div class="panel-footer">
|
||||
<a href="{% url "control:event.log" event=request.event.slug organizer=request.event.organizer.slug %}">
|
||||
{% trans "Show more logs" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -169,13 +169,15 @@
|
||||
</p>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-default btn-lg" name="preview" value="preview" formtarget="_blank">
|
||||
{% trans "Save and show preview" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
{% if "event.settings.invoicing:write" in request.eventpermset %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-default btn-lg" name="preview" value="preview" formtarget="_blank">
|
||||
{% trans "Save and show preview" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -41,14 +41,17 @@
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% url 'control:event.settings.payment.provider' event=request.event.slug organizer=request.organizer.slug provider=provider.identifier %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-cog"></span>
|
||||
{% trans "Settings" %}
|
||||
</a>
|
||||
{% if "event.settings.payment:write" in request.eventpermset %}
|
||||
<a href="{% url 'control:event.settings.payment.provider' event=request.event.slug organizer=request.organizer.slug provider=provider.identifier %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-cog"></span>
|
||||
{% trans "Settings" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if "event.settings.general:write" in request.eventpermset %}
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<br>
|
||||
@@ -58,6 +61,7 @@
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -83,10 +87,12 @@
|
||||
{% bootstrap_field form.payment_explanation layout="control" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
{% if "event.settings.payment:write" in request.eventpermset %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -23,8 +23,10 @@
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<a href="{% url "control:event.settings.tax.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new tax rule" %}</a>
|
||||
{% if "event.settings.tax:write" in request.eventpermset %}
|
||||
<a href="{% url "control:event.settings.tax.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new tax rule" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="table-responsive">
|
||||
@@ -42,10 +44,14 @@
|
||||
{% for tr in taxrules %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong><a
|
||||
href="{% url "control:event.settings.tax.edit" organizer=request.event.organizer.slug event=request.event.slug rule=tr.id %}">
|
||||
{{ tr.internal_name|default:tr.name }}
|
||||
</a></strong>
|
||||
{% if "event.settings.tax:write" in request.eventpermset %}
|
||||
<strong><a
|
||||
href="{% url "control:event.settings.tax.edit" organizer=request.event.organizer.slug event=request.event.slug rule=tr.id %}">
|
||||
{{ tr.internal_name|default:tr.name }}
|
||||
</a></strong>
|
||||
{% else %}
|
||||
<strong>{{ tr.internal_name|default:tr.name }}</strong>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if tr.default %}
|
||||
@@ -53,7 +59,7 @@
|
||||
<span class="fa fa-check"></span>
|
||||
{% trans "Default" %}
|
||||
</span>
|
||||
{% else %}
|
||||
{% elif "event.settings.tax:write" in request.eventpermset %}
|
||||
<form class="form-inline" method="post"
|
||||
action="{% url "control:event.settings.tax.default" organizer=request.event.organizer.slug event=request.event.slug rule=tr.id %}">
|
||||
{% csrf_token %}
|
||||
@@ -83,10 +89,12 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% url "control:event.settings.tax.edit" organizer=request.event.organizer.slug event=request.event.slug rule=tr.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.settings.tax.delete" organizer=request.event.organizer.slug event=request.event.slug rule=tr.id %}"
|
||||
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% if "event.settings.tax:write" in request.eventpermset %}
|
||||
<a href="{% url "control:event.settings.tax.edit" organizer=request.event.organizer.slug event=request.event.slug rule=tr.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.settings.tax.delete" organizer=request.event.organizer.slug event=request.event.slug rule=tr.id %}"
|
||||
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -94,9 +102,11 @@
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<a href="{% url "control:event.settings.tax.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new tax rule" %}
|
||||
</a>
|
||||
{% if "event.settings.tax:write" in request.eventpermset %}
|
||||
<a href="{% url "control:event.settings.tax.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new tax rule" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
@@ -111,10 +121,12 @@
|
||||
{% bootstrap_field form.tax_rounding layout="control" %}
|
||||
{% bootstrap_field form.display_net_prices layout="control" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
{% if "event.settings.tax:write" in request.eventpermset %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -16,14 +16,18 @@
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new category" %}</a>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new category" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new category" %}
|
||||
</a>
|
||||
</p>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new category" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="table-responsive">
|
||||
@@ -39,7 +43,11 @@
|
||||
{% for c in categories %}
|
||||
<tr data-dnd-id="{{ c.id }}">
|
||||
<td>
|
||||
<strong><a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}">{{ c.internal_name|default:c.name }}</a></strong>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<strong><a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}">{{ c.internal_name|default:c.name }}</a></strong>
|
||||
{% else %}
|
||||
<strong>{{ c.internal_name|default:c.name }}</strong>
|
||||
{% endif %}
|
||||
<br>
|
||||
<small class="text-muted">
|
||||
#{{ c.pk }}
|
||||
@@ -49,15 +57,17 @@
|
||||
{{ c.get_category_type_display }}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
<button title="{% trans "Move up" %}" formaction="{% url "control:event.items.categories.up" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
|
||||
<button title="{% trans "Move down" %}" formaction="{% url "control:event.items.categories.down" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
|
||||
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
|
||||
<a title="{% trans "Edit" %}" href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ c.id }}"
|
||||
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
|
||||
<span class="fa fa-copy"></span>
|
||||
</a>
|
||||
<a title="{% trans "Delete" %}" href="{% url "control:event.items.categories.delete" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<button title="{% trans "Move up" %}" formaction="{% url "control:event.items.categories.up" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
|
||||
<button title="{% trans "Move down" %}" formaction="{% url "control:event.items.categories.down" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
|
||||
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
|
||||
<a title="{% trans "Edit" %}" href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ c.id }}"
|
||||
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
|
||||
<span class="fa fa-copy"></span>
|
||||
</a>
|
||||
<a title="{% trans "Delete" %}" href="{% url "control:event.items.categories.delete" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -39,15 +39,19 @@
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new discount" %}</a>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new discount" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new discount" %}
|
||||
</a>
|
||||
</p>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new discount" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="table-responsive">
|
||||
@@ -70,8 +74,12 @@
|
||||
{% else %}
|
||||
<del>
|
||||
{% endif %}
|
||||
<a href="{% url "control:event.items.discounts.edit" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}">
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.discounts.edit" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}">
|
||||
{{ d.internal_name }}</a>
|
||||
{% else %}
|
||||
{{ d.internal_name }}
|
||||
{% endif %}
|
||||
{% if d.active %}
|
||||
</strong>
|
||||
{% else %}
|
||||
@@ -134,23 +142,25 @@
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-right flip">
|
||||
<button formaction="{% url "control:event.items.discounts.up" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
|
||||
class="btn btn-default btn-sm sortable-up" title="{% trans "Move up" %}"
|
||||
{% if forloop.counter0 == 0 and not page_obj.has_previous %}
|
||||
disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
|
||||
<button formaction="{% url "control:event.items.discounts.down" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
|
||||
class="btn btn-default btn-sm sortable-down" title="{% trans "Move down" %}"
|
||||
{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
|
||||
<a href="{% url "control:event.items.discounts.edit" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ d.id }}"
|
||||
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
|
||||
<span class="fa fa-copy"></span>
|
||||
</a>
|
||||
<a href="{% url "control:event.items.discounts.delete" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
|
||||
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<button formaction="{% url "control:event.items.discounts.up" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
|
||||
class="btn btn-default btn-sm sortable-up" title="{% trans "Move up" %}"
|
||||
{% if forloop.counter0 == 0 and not page_obj.has_previous %}
|
||||
disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
|
||||
<button formaction="{% url "control:event.items.discounts.down" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
|
||||
class="btn btn-default btn-sm sortable-down" title="{% trans "Move down" %}"
|
||||
{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
|
||||
<a href="{% url "control:event.items.discounts.edit" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ d.id }}"
|
||||
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
|
||||
<span class="fa fa-copy"></span>
|
||||
</a>
|
||||
<a href="{% url "control:event.items.discounts.delete" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
|
||||
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -21,14 +21,18 @@
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new product" %}</a>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new product" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new product" %}</a>
|
||||
</p>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new product" %}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="table-responsive">
|
||||
@@ -51,7 +55,9 @@
|
||||
<tbody>
|
||||
<tr class="sortable-disabled"><th colspan="9" scope="colgroup" class="text-muted">
|
||||
{{ c.internal_name|default:c.name }}{% if c.category_type != "normal" %} <span class="font-normal">({{ c.get_category_type_display }})</span>{% endif %}
|
||||
<a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" title="{% trans "Edit" %}"><span class="fa fa-edit fa-fw"></span></a>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" title="{% trans "Edit" %}"><span class="fa fa-edit fa-fw"></span></a>
|
||||
{% endif %}
|
||||
</th></tr>
|
||||
</tbody>
|
||||
{% endif %}
|
||||
@@ -62,7 +68,11 @@
|
||||
<tr data-dnd-id="{{ i.id }}" {% if not i.active %}class="row-muted"{% endif %}>
|
||||
<td><strong>
|
||||
{% if not i.active %}<strike>{% endif %}
|
||||
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}">{{ i }}</a>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}">{{ i }}</a>
|
||||
{% else %}
|
||||
{{ i }}
|
||||
{% endif %}
|
||||
{% if not i.active %}</strike>{% endif %}
|
||||
</strong>
|
||||
<br>
|
||||
@@ -158,12 +168,14 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip col-actions">
|
||||
<button title="{% trans "Move up" %}" formaction="{% url "control:event.items.up" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
|
||||
<button title="{% trans "Move down" %}" formaction="{% url "control:event.items.down" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
|
||||
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
|
||||
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm" title="{% trans "Edit" %}"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ i.id }}" class="btn btn-default btn-sm" title="{% trans "Clone" %}" data-toggle="tooltip"><i class="fa fa-copy"></i></a>
|
||||
<a href="{% url "control:event.items.delete" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-danger btn-sm" title="{% trans "Delete" %}"><i class="fa fa-trash"></i></a>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<button title="{% trans "Move up" %}" formaction="{% url "control:event.items.up" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
|
||||
<button title="{% trans "Move down" %}" formaction="{% url "control:event.items.down" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
|
||||
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
|
||||
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm" title="{% trans "Edit" %}"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ i.id }}" class="btn btn-default btn-sm" title="{% trans "Clone" %}" data-toggle="tooltip"><i class="fa fa-copy"></i></a>
|
||||
<a href="{% url "control:event.items.delete" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-danger btn-sm" title="{% trans "Delete" %}"><i class="fa fa-trash"></i></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -7,60 +7,57 @@
|
||||
{% block inside %}
|
||||
<h1>
|
||||
{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}
|
||||
<a href="{% url "control:event.items.questions.edit" event=request.event.slug organizer=request.event.organizer.slug question=question.pk %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-edit"></span>
|
||||
{% trans "Edit question" %}
|
||||
</a>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.questions.edit" event=request.event.slug organizer=request.event.organizer.slug question=question.pk %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-edit"></span>
|
||||
{% trans "Edit question" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</h1>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Filter" %}</h3>
|
||||
</div>
|
||||
<form class="panel-body filter-form" action="" method="get">
|
||||
<div class="row">
|
||||
<div class="col-lg-2 col-sm-6 col-xs-6">
|
||||
<select name="status" class="form-control">
|
||||
<option value="" {% if request.GET.status == "" %}selected="selected"{% endif %}>{% trans "All orders" %}</option>
|
||||
<option value="p" {% if request.GET.status == "p" %}selected="selected"{% endif %}>{% trans "Paid" %}</option>
|
||||
<option value="pv" {% if request.GET.status == "pv" %}selected="selected"{% endif %}>{% trans "Paid or confirmed" %}</option>
|
||||
<option value="n" {% if request.GET.status == "n" %}selected="selected"{% endif %}>{% trans "Pending" %}</option>
|
||||
<option value="np" {% if request.GET.status == "np" or "status" not in request.GET %}selected="selected"{% endif %}>{% trans "Pending or paid" %}</option>
|
||||
<option value="o" {% if request.GET.status == "o" %}selected="selected"{% endif %}>{% trans "Pending (overdue)" %}</option>
|
||||
<option value="e" {% if request.GET.status == "e" %}selected="selected"{% endif %}>{% trans "Expired" %}</option>
|
||||
<option value="ne" {% if request.GET.status == "ne" %}selected="selected"{% endif %}>{% trans "Pending or expired" %}</option>
|
||||
<option value="c" {% if request.GET.status == "c" %}selected="selected"{% endif %}>{% trans "Canceled" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-5 col-sm-6 col-xs-6">
|
||||
<select name="item" class="form-control">
|
||||
<option value="">{% trans "All products" %}</option>
|
||||
{% for item in items %}
|
||||
<option value="{{ item.id }}"
|
||||
{% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}>
|
||||
{{ item.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% if request.event.has_subevents %}
|
||||
<div class="col-lg-5 col-sm-6 col-xs-6">
|
||||
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
|
||||
{% if 'event.orders:read' in request.eventpermset %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Filter" %}</h3>
|
||||
</div>
|
||||
<form class="panel-body filter-form" action="" method="get">
|
||||
<div class="row">
|
||||
<div class="col-md-2 col-xs-6">
|
||||
{% bootstrap_field form.status %}
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-6">
|
||||
{% bootstrap_field form.item %}
|
||||
</div>
|
||||
{% if has_subevents %}
|
||||
<div class="col-md-3 col-xs-6">
|
||||
{% bootstrap_field form.subevent %}
|
||||
</div>
|
||||
<div class="col-md-4 col-xs-6">
|
||||
{% bootstrap_field form.date_range %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button class="btn btn-primary btn-lg" type="submit">
|
||||
<span class="fa fa-filter"></span>
|
||||
{% trans "Filter" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button class="btn btn-primary btn-lg" type="submit">
|
||||
<span class="fa fa-filter"></span>
|
||||
{% trans "Filter" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
{% if not stats %}
|
||||
{% if 'event.orders:read' not in request.eventpermset %}
|
||||
<div class="empty-collection col-md-10 col-xs-12">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
No permission to view answers.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
{% elif not stats %}
|
||||
<div class="empty-collection col-md-10 col-xs-12">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
@@ -10,10 +10,12 @@
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new question" %}
|
||||
</a>
|
||||
</p>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new question" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
@@ -24,7 +26,9 @@
|
||||
<th class="iconcol"></th>
|
||||
<th class="iconcol"></th>
|
||||
<th>{% trans "Products" %}</th>
|
||||
<th class="action-col-2"></th>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<th class="action-col-2"></th>
|
||||
{% endif %}
|
||||
<th class="action-col-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -79,16 +83,22 @@
|
||||
<small>{% trans "All personalized products" %}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="dnd-container">
|
||||
</td>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<td class="dnd-container">
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-right flip">
|
||||
{% if q.pk %}
|
||||
<a href="{% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-bar-chart"></i></a>
|
||||
<a href="{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.items.questions.delete" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.items.questions.delete" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a href="{% url "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}#tab-0-2-open"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-wrench"></i></a>
|
||||
{% if 'event.settings.general:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}#tab-0-2-open"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-wrench"></i></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
{% block inside %}
|
||||
<h1>
|
||||
{% blocktrans with name=quota.name %}Quota: {{ name }}{% endblocktrans %}
|
||||
{% if 'can_change_items' in request.eventpermset %}
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.quotas.edit" event=request.event.slug organizer=request.event.organizer.slug quota=quota.pk %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-edit"></span>
|
||||
|
||||
@@ -30,14 +30,18 @@
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new quota" %}</a>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new quota" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new quota" %}
|
||||
</a>
|
||||
</p>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new quota" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
@@ -91,12 +95,14 @@
|
||||
<td>{% if q.size == None %}Unlimited{% else %}{{ q.size }}{% endif %}</td>
|
||||
<td>{% include "pretixcontrol/items/fragment_quota_availability.html" with availability=q.cached_avail closed=q.closed %}</td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% url "control:event.items.quotas.edit" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ q.id }}"
|
||||
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
|
||||
<span class="fa fa-copy"></span>
|
||||
</a>
|
||||
<a href="{% url "control:event.items.quotas.delete" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.quotas.edit" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ q.id }}"
|
||||
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
|
||||
<span class="fa fa-copy"></span>
|
||||
</a>
|
||||
<a href="{% url "control:event.items.quotas.delete" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
{% endif %}
|
||||
{% include "pretixcontrol/orders/fragment_order_status.html" with order=order class="pull-right flip" %}
|
||||
</h1>
|
||||
{% if 'can_change_orders' in request.eventpermset %}
|
||||
{% if 'event.orders:write' in request.eventpermset %}
|
||||
<form action="{% url "control:event.order.transition" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}"
|
||||
method="post">
|
||||
{% csrf_token %}
|
||||
@@ -193,7 +193,7 @@
|
||||
<dt>{% trans "Order locale" %}</dt>
|
||||
<dd>
|
||||
{{ display_locale }}
|
||||
{% if "can_change_orders" in request.eventpermset %}
|
||||
{% if "event.orders:write" in request.eventpermset %}
|
||||
<a href="{% url "control:event.order.locale" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
|
||||
<span class="fa fa-edit"></span>
|
||||
</a>
|
||||
@@ -220,7 +220,7 @@
|
||||
{{ order.customer.identifier }} – {{ order.customer.email }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if "can_change_orders" in request.eventpermset %}
|
||||
{% if "event.orders:write" in request.eventpermset %}
|
||||
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
|
||||
<span class="fa fa-edit"></span>
|
||||
</a>
|
||||
@@ -233,7 +233,7 @@
|
||||
{% if order.email and order.email_known_to_work %}
|
||||
<span class="fa fa-check-circle text-success" data-toggle="tooltip" title="{% trans "We know that this email address works because the user clicked a link we sent them." %}"></span>
|
||||
{% endif %}
|
||||
{% if "can_change_orders" in request.eventpermset %}
|
||||
{% if "event.orders:write" in request.eventpermset %}
|
||||
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
|
||||
<span class="fa fa-edit"></span>
|
||||
</a>
|
||||
@@ -257,7 +257,7 @@
|
||||
<dt>{% trans "Phone number" %}</dt>
|
||||
<dd>
|
||||
{{ order.phone|default_if_none:""|phone_format }}
|
||||
{% if "can_change_orders" in request.eventpermset %}
|
||||
{% if "event.orders:write" in request.eventpermset %}
|
||||
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
|
||||
<span class="fa fa-edit"></span>
|
||||
</a>
|
||||
@@ -319,7 +319,7 @@
|
||||
<span class="fa fa-check text-success fa-stack-1x fa-stack-shifted"></span>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if i.transmission_status != "inflight" %}
|
||||
{% if i.transmission_status != "inflight" and "event.orders:write" in request.eventpermset %}
|
||||
<form class="form-inline helper-display-inline" method="post"
|
||||
action="{% url "control:event.order.retransmitinvoice" event=request.event.slug organizer=request.event.organizer.slug code=order.code id=i.pk %}">
|
||||
{% csrf_token %}
|
||||
@@ -334,7 +334,7 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if not i.canceled %}
|
||||
{% if i.regenerate_allowed %}
|
||||
{% if i.regenerate_allowed and "event.orders:write" in request.eventpermset %}
|
||||
<form class="form-inline helper-display-inline" method="post"
|
||||
action="{% url "control:event.order.regeninvoice" event=request.event.slug organizer=request.event.organizer.slug code=order.code id=i.pk %}">
|
||||
{% csrf_token %}
|
||||
@@ -344,7 +344,7 @@
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if not i.is_cancellation %}
|
||||
{% if not i.is_cancellation and "event.orders:write" in request.eventpermset %}
|
||||
<form class="form-inline helper-display-inline" method="post"
|
||||
action="{% url "control:event.order.reissueinvoice" event=request.event.slug organizer=request.event.organizer.slug code=order.code id=i.pk %}">
|
||||
{% csrf_token %}
|
||||
@@ -371,7 +371,7 @@
|
||||
<br/>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if can_generate_invoice and 'can_change_orders' in request.eventpermset %}
|
||||
{% if can_generate_invoice and 'event.orders:write' in request.eventpermset %}
|
||||
<br/>
|
||||
<form class="form-inline helper-display-inline" method="post"
|
||||
action="{% url "control:event.order.geninvoice" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
@@ -382,7 +382,7 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% elif can_generate_invoice and 'can_change_orders' in request.eventpermset %}
|
||||
{% elif can_generate_invoice and 'event.orders:write' in request.eventpermset %}
|
||||
<dt>{% trans "Invoices" %}</dt>
|
||||
<dd>
|
||||
<form class="form-inline helper-display-inline" method="post"
|
||||
@@ -400,7 +400,7 @@
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<div class="pull-right flip">
|
||||
{% if 'can_change_orders' in request.eventpermset %}
|
||||
{% if 'event.orders:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.order.info" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
<span class="fa fa-edit"></span>
|
||||
{% trans "Change answers" %}
|
||||
@@ -893,7 +893,7 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if order.payment_refund_sum > 0 and "can_change_orders" in request.eventpermset %}
|
||||
{% if order.payment_refund_sum > 0 and "event.orders:write" in request.eventpermset %}
|
||||
<a href="{% url "control:event.order.refunds.start" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default">
|
||||
{% trans "Create a refund" %}
|
||||
</a>
|
||||
@@ -1012,7 +1012,7 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="pull-right flip">
|
||||
{% if 'can_change_orders' in request.eventpermset %}
|
||||
{% if 'event.orders:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.order.info" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
<span class="fa fa-edit"></span>
|
||||
{% trans "Change" %}
|
||||
@@ -1088,7 +1088,7 @@
|
||||
{% bootstrap_field comment_form.custom_followup_at %}
|
||||
{% bootstrap_field comment_form.checkin_attention show_help=True show_label=False %}
|
||||
{% bootstrap_field comment_form.checkin_text show_help=True show_label=False %}
|
||||
{% if "can_change_orders" in request.eventpermset %}
|
||||
{% if "event.orders:write" in request.eventpermset %}
|
||||
<button class="btn btn-default">
|
||||
{% trans "Update comment" %}
|
||||
</button>
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
<table class="table table-condensed table-hover table-orders">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if "can_change_orders" in request.eventpermset %}
|
||||
{% if "event.orders:write" in request.eventpermset %}
|
||||
<th>
|
||||
<label aria-label="{% trans "select all rows for batch-operation" %}"
|
||||
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
|
||||
@@ -154,7 +154,7 @@
|
||||
<a href="?{% url_replace request 'ordering' 'status' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
</tr>
|
||||
{% if page_obj.paginator.num_pages > 1 and "can_change_orders" in request.eventpermset %}
|
||||
{% if page_obj.paginator.num_pages > 1 and "event.orders:write" in request.eventpermset %}
|
||||
<tr class="table-select-all warning hidden">
|
||||
<td>
|
||||
<input type="checkbox" name="__ALL" id="__all"
|
||||
@@ -171,7 +171,7 @@
|
||||
<tbody>
|
||||
{% for o in orders %}
|
||||
<tr>
|
||||
{% if "can_change_orders" in request.eventpermset %}
|
||||
{% if "event.orders:write" in request.eventpermset %}
|
||||
<td>
|
||||
<label aria-label="{% trans "select row for batch-operation" %}"
|
||||
class="batch-select-label"><input type="checkbox" name="order"
|
||||
@@ -281,7 +281,7 @@
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
{% if "can_change_orders" in request.eventpermset %}
|
||||
{% if "event.orders:write" in request.eventpermset %}
|
||||
<div class="batch-select-actions">
|
||||
<div class="btn-group dropup">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"
|
||||
|
||||
@@ -100,28 +100,30 @@
|
||||
{{ r.amount|money:request.event.currency }}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
{% if r.state == "transit" or r.state == "created" %}
|
||||
<a href="{% url "control:event.order.refunds.cancel" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
|
||||
class="btn btn-danger btn-xs" data-toggle="tooltip">
|
||||
<span class="fa fa-times"></span>
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<a href="{% url "control:event.order.refunds.done" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
|
||||
class="btn btn-primary btn-xs" data-toggle="tooltip">
|
||||
<span class="fa fa-check"></span>
|
||||
{% trans "Confirm as done" %}
|
||||
</a>
|
||||
{% if "event.orders:write" in request.eventpermset %}
|
||||
{% if r.state == "transit" or r.state == "created" %}
|
||||
<a href="{% url "control:event.order.refunds.cancel" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
|
||||
class="btn btn-danger btn-xs" data-toggle="tooltip">
|
||||
<span class="fa fa-times"></span>
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<a href="{% url "control:event.order.refunds.done" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
|
||||
class="btn btn-primary btn-xs" data-toggle="tooltip">
|
||||
<span class="fa fa-check"></span>
|
||||
{% trans "Confirm as done" %}
|
||||
</a>
|
||||
{% elif r.state == "external" %}
|
||||
<a href="{% url "control:event.order.refunds.cancel" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
|
||||
class="btn btn-default btn-xs" data-toggle="tooltip">
|
||||
<span class="fa fa-times"></span>
|
||||
{% trans "Ignore" %}
|
||||
</a>
|
||||
<a href="{% url "control:event.order.refunds.process" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
|
||||
class="btn btn-primary btn-xs" data-toggle="tooltip">
|
||||
<span class="fa fa-check"></span>
|
||||
{% trans "Process refund" %}
|
||||
</a>
|
||||
<a href="{% url "control:event.order.refunds.cancel" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
|
||||
class="btn btn-default btn-xs" data-toggle="tooltip">
|
||||
<span class="fa fa-times"></span>
|
||||
{% trans "Ignore" %}
|
||||
</a>
|
||||
<a href="{% url "control:event.order.refunds.process" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
|
||||
class="btn btn-primary btn-xs" data-toggle="tooltip">
|
||||
<span class="fa fa-check"></span>
|
||||
{% trans "Process refund" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -93,16 +93,18 @@
|
||||
{% endif %}
|
||||
</dl>
|
||||
</form>
|
||||
<div class="text-right">
|
||||
<a href="{% url "control:organizer.customer.edit" organizer=request.organizer.slug customer=customer.identifier %}"
|
||||
class="btn btn-default">
|
||||
<i class="fa fa-edit"></i> {% trans "Edit" %}
|
||||
</a>
|
||||
<a href="{% url "control:organizer.customer.anonymize" organizer=request.organizer.slug customer=customer.identifier %}"
|
||||
class="btn btn-danger">
|
||||
<i class="fa fa-trash"></i> {% trans "Anonymize" %}
|
||||
</a>
|
||||
</div>
|
||||
{% if "organizer.customers:write" in request.orgapermset %}
|
||||
<div class="text-right">
|
||||
<a href="{% url "control:organizer.customer.edit" organizer=request.organizer.slug customer=customer.identifier %}"
|
||||
class="btn btn-default">
|
||||
<i class="fa fa-edit"></i> {% trans "Edit" %}
|
||||
</a>
|
||||
<a href="{% url "control:organizer.customer.anonymize" organizer=request.organizer.slug customer=customer.identifier %}"
|
||||
class="btn btn-danger">
|
||||
<i class="fa fa-trash"></i> {% trans "Anonymize" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default items">
|
||||
@@ -162,35 +164,39 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% url "control:organizer.customer.membership.edit" organizer=request.organizer.slug customer=customer.identifier id=m.pk %}"
|
||||
data-toggle="tooltip"
|
||||
title="{% trans "Edit" %}"
|
||||
class="btn btn-default">
|
||||
<i class="fa fa-edit"></i>
|
||||
</a>
|
||||
{% if m.testmode %}
|
||||
<a href="{% url "control:organizer.customer.membership.delete" organizer=request.organizer.slug customer=customer.identifier id=m.pk %}"
|
||||
{% if "organizer.customers:write" in request.orgapermset %}
|
||||
<a href="{% url "control:organizer.customer.membership.edit" organizer=request.organizer.slug customer=customer.identifier id=m.pk %}"
|
||||
data-toggle="tooltip"
|
||||
title="{% trans "Delete" %}"
|
||||
class="btn btn-danger">
|
||||
<i class="fa fa-trash"></i>
|
||||
title="{% trans "Edit" %}"
|
||||
class="btn btn-default">
|
||||
<i class="fa fa-edit"></i>
|
||||
</a>
|
||||
{% if m.testmode %}
|
||||
<a href="{% url "control:organizer.customer.membership.delete" organizer=request.organizer.slug customer=customer.identifier id=m.pk %}"
|
||||
data-toggle="tooltip"
|
||||
title="{% trans "Delete" %}"
|
||||
class="btn btn-danger">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<a href="{% url "control:organizer.customer.membership.add" organizer=request.organizer.slug customer=customer.identifier %}"
|
||||
class="btn btn-default">
|
||||
<i class="fa fa-plus"></i>
|
||||
{% trans "Add membership" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
{% if "organizer.customers:write" in request.orgapermset %}
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<a href="{% url "control:organizer.customer.membership.add" organizer=request.organizer.slug customer=customer.identifier %}"
|
||||
class="btn btn-default">
|
||||
<i class="fa fa-plus"></i>
|
||||
{% trans "Add membership" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-default items">
|
||||
@@ -300,14 +306,18 @@
|
||||
{% for gc in gift_cards %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url "control:organizer.giftcard" organizer=organizer.slug giftcard=gc.id %}">
|
||||
<strong>{{ gc.secret }}</strong></a>
|
||||
{% if gc.testmode %}
|
||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||
{% endif %}
|
||||
{% if gc.expired %}
|
||||
<span class="label label-danger">{% trans "Expired" %}</span>
|
||||
{% endif %}
|
||||
{% if "organizer.giftcards:read" in request.orgapermset %}
|
||||
<a href="{% url "control:organizer.giftcard" organizer=organizer.slug giftcard=gc.id %}">
|
||||
<strong>{{ gc.secret }}</strong></a>
|
||||
{% else %}
|
||||
<strong>{{ gc.secret|slice:":3" }}…</strong>
|
||||
{% endif %}
|
||||
{% if gc.testmode %}
|
||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||
{% endif %}
|
||||
{% if gc.expired %}
|
||||
<span class="label label-danger">{% trans "Expired" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ gc.issuance|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
<td>{% if gc.expires %}{{ gc.expires|date:"SHORT_DATETIME_FORMAT" }}{% endif %}</td>
|
||||
@@ -316,10 +326,12 @@
|
||||
<p class="text-right">{{ gc.value|money:gc.currency }}</p>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url "control:organizer.giftcard" organizer=organizer.slug giftcard=gc.id %}"
|
||||
class="btn btn-default btn-sm" data-toggle="tooltip" title="{% trans "Details" %}">
|
||||
<i class="fa fa-eye"></i>
|
||||
</a>
|
||||
{% if "organizer.giftcards:read" in request.orgapermset %}
|
||||
<a href="{% url "control:organizer.giftcard" organizer=organizer.slug giftcard=gc.id %}"
|
||||
class="btn btn-default btn-sm" data-toggle="tooltip" title="{% trans "Details" %}">
|
||||
<i class="fa fa-eye"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
No customer accounts have been created yet.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<a href="{% url "control:organizer.customer.create" organizer=request.organizer.slug %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new customer" %}</a>
|
||||
{% if "organizer.customers:write" in request.orgapermset %}
|
||||
<a href="{% url "control:organizer.customer.create" organizer=request.organizer.slug %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new customer" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="panel panel-default">
|
||||
@@ -43,10 +45,12 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<p>
|
||||
<a href="{% url "control:organizer.customer.create" organizer=request.organizer.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new customer" %}</a>
|
||||
</p>
|
||||
{% if "organizer.customers:write" in request.orgapermset %}
|
||||
<p>
|
||||
<a href="{% url "control:organizer.customer.create" organizer=request.organizer.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new customer" %}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{% blocktrans with name=request.organizer.name %}Organizer: {{ name }}{% endblocktrans %}
|
||||
</h1>
|
||||
{% if events|length == 0 and not filter_form.filtered %}
|
||||
{% if "can_create_events" in request.orgapermset %}
|
||||
{% if "organizer.events:create" in request.orgapermset %}
|
||||
<p>
|
||||
<a href="{% url "control:events.add" %}?organizer={{ request.organizer.slug }}" class="btn btn-primary">
|
||||
<span class="fa fa-plus"></span>
|
||||
@@ -50,7 +50,7 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% if "can_create_events" in request.orgapermset %}
|
||||
{% if "organizer.events:create" in request.orgapermset %}
|
||||
<p>
|
||||
<a href="{% url "control:events.add" %}?organizer={{ request.organizer.slug }}" class="btn btn-primary">
|
||||
<span class="fa fa-plus"></span>
|
||||
@@ -125,7 +125,7 @@
|
||||
data-toggle="tooltip">
|
||||
<span class="fa fa-eye"></span>
|
||||
</a>
|
||||
{% if "can_create_events" in request.orgapermset %}
|
||||
{% if "organizer.events:create" in request.orgapermset %}
|
||||
<a href="{% url "control:events.add" %}?clone={{ e.pk }}" class="btn btn-sm btn-default"
|
||||
title="{% trans "Clone event" %}" data-toggle="tooltip">
|
||||
<span class="fa fa-copy"></span>
|
||||
|
||||
@@ -51,10 +51,12 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<p>
|
||||
<a href="{% url "control:organizer.device.add" organizer=request.organizer.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Connect a device" %}</a>
|
||||
</p>
|
||||
{% if "organizer.devices:write" in request.orgapermset %}
|
||||
<p>
|
||||
<a href="{% url "control:organizer.device.add" organizer=request.organizer.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Connect a device" %}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<form action="{% url "control:organizer.device.bulk_edit" organizer=request.organizer.slug %}" method="post">
|
||||
{% csrf_token %}
|
||||
{% for field in filter_form %}
|
||||
@@ -64,10 +66,12 @@
|
||||
<table class="table table-condensed table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<label aria-label="{% trans "select all rows for batch-operation" %}"
|
||||
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
|
||||
</th>
|
||||
{% if "organizer.devices:write" in request.orgapermset %}
|
||||
<th>
|
||||
<label aria-label="{% trans "select all rows for batch-operation" %}"
|
||||
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
|
||||
</th>
|
||||
{% endif %}
|
||||
<th>{% trans "Device ID" %}
|
||||
<a href="?{% url_replace request 'ordering' '-device_id' %}"><i
|
||||
class="fa fa-caret-down"></i></a>
|
||||
@@ -105,12 +109,14 @@
|
||||
<tbody>
|
||||
{% for d in devices %}
|
||||
<tr {% if d.revoked %}class="text-muted"{% endif %}>
|
||||
<td>
|
||||
<label aria-label="{% trans "select row for batch-operation" %}"
|
||||
class="batch-select-label"><input type="checkbox" name="device"
|
||||
class="batch-select-checkbox"
|
||||
value="{{ d.pk }}"/></label>
|
||||
</td>
|
||||
{% if "organizer.devices:write" in request.orgapermset %}
|
||||
<td>
|
||||
<label aria-label="{% trans "select row for batch-operation" %}"
|
||||
class="batch-select-label"><input type="checkbox" name="device"
|
||||
class="batch-select-checkbox"
|
||||
value="{{ d.pk }}"/></label>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
{{ d.device_id }}
|
||||
</td>
|
||||
@@ -158,15 +164,17 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
{% if not d.initialized %}
|
||||
<a href="{% url "control:organizer.device.connect" organizer=request.organizer.slug device=d.id %}"
|
||||
class="btn btn-primary btn-sm"><i class="fa fa-link"></i>
|
||||
{% trans "Connect" %}</a>
|
||||
{% endif %}
|
||||
{% if not d.initialized or d.api_token %}
|
||||
<a href="{% url "control:organizer.device.revoke" organizer=request.organizer.slug device=d.id %}"
|
||||
class="btn btn-default btn-sm">
|
||||
{% trans "Revoke access" %}</a>
|
||||
{% if "organizer.devices:write" in request.orgapermset %}
|
||||
{% if not d.initialized %}
|
||||
<a href="{% url "control:organizer.device.connect" organizer=request.organizer.slug device=d.id %}"
|
||||
class="btn btn-primary btn-sm"><i class="fa fa-link"></i>
|
||||
{% trans "Connect" %}</a>
|
||||
{% endif %}
|
||||
{% if not d.initialized or d.api_token %}
|
||||
<a href="{% url "control:organizer.device.revoke" organizer=request.organizer.slug device=d.id %}"
|
||||
class="btn btn-default btn-sm">
|
||||
{% trans "Revoke access" %}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if d.initialized %}
|
||||
<a href="{% url "control:organizer.device.logs" organizer=request.organizer.slug device=d.id %}"
|
||||
@@ -175,19 +183,23 @@
|
||||
{% trans "Logs" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url "control:organizer.device.edit" organizer=request.organizer.slug device=d.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
{% if "organizer.devices:write" in request.orgapermset %}
|
||||
<a href="{% url "control:organizer.device.edit" organizer=request.organizer.slug device=d.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="batch-select-actions">
|
||||
<button type="submit" class="btn btn-primary btn-save" name="action" value="edit">
|
||||
<i class="fa fa-edit"></i>{% trans "Edit selected" %}
|
||||
</button>
|
||||
</div>
|
||||
{% if "organizer.devices:write" in request.orgapermset %}
|
||||
<div class="batch-select-actions">
|
||||
<button type="submit" class="btn btn-primary btn-save" name="action" value="edit">
|
||||
<i class="fa fa-edit"></i>{% trans "Edit selected" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endif %}
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
<p>
|
||||
{% trans "The list below shows gates that you can use to group check-in devices." %}
|
||||
</p>
|
||||
<a href="{% url "control:organizer.gate.add" organizer=request.organizer.slug %}" class="btn btn-default">
|
||||
<span class="fa fa-plus"></span>
|
||||
{% trans "Create a new gate" %}
|
||||
</a>
|
||||
{% if "organizer.devices:write" in request.orgapermset %}
|
||||
<a href="{% url "control:organizer.gate.add" organizer=request.organizer.slug %}" class="btn btn-default">
|
||||
<span class="fa fa-plus"></span>
|
||||
{% trans "Create a new gate" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -21,15 +23,21 @@
|
||||
{% for g in gates %}
|
||||
<tr>
|
||||
<td><strong>
|
||||
<a href="{% url "control:organizer.gate.edit" organizer=request.organizer.slug gate=g.id %}">
|
||||
{% if "organizer.devices:write" in request.orgapermset %}
|
||||
<a href="{% url "control:organizer.gate.edit" organizer=request.organizer.slug gate=g.id %}">
|
||||
{{ g.name }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ g.name }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</strong></td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% url "control:organizer.gate.edit" organizer=request.organizer.slug gate=g.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:organizer.gate.delete" organizer=request.organizer.slug gate=g.id %}"
|
||||
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% if "organizer.devices:write" in request.orgapermset %}
|
||||
<a href="{% url "control:organizer.gate.edit" organizer=request.organizer.slug gate=g.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:organizer.gate.delete" organizer=request.organizer.slug gate=g.id %}"
|
||||
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -10,10 +10,12 @@
|
||||
{% if card.testmode %}
|
||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||
{% endif %}
|
||||
<a href="{% url "control:organizer.giftcard.edit" organizer=request.organizer.slug giftcard=card.id %}"
|
||||
class="btn btn-default">
|
||||
<i class="fa fa-edit"></i> {% trans "Edit" %}
|
||||
</a>
|
||||
{% if "organizer.giftcards:write" in request.orgapermset %}
|
||||
<a href="{% url "control:organizer.giftcard.edit" organizer=request.organizer.slug giftcard=card.id %}"
|
||||
class="btn btn-default">
|
||||
<i class="fa fa-edit"></i> {% trans "Edit" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-xs-12">
|
||||
@@ -112,22 +114,24 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<input type="text" class="form-control helper-display-block" placeholder="{% trans "Text" %}"
|
||||
name="text">
|
||||
</td>
|
||||
<td class="text-right form-inline">
|
||||
<input type="text" class="form-control input-sm" placeholder="{% trans "Value" %}" name="value">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="fa fa-plus"></span>
|
||||
</button>
|
||||
</td>
|
||||
{% if "organizer.giftcards:write" in request.orgapermset %}
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<input type="text" class="form-control helper-display-block" placeholder="{% trans "Text" %}"
|
||||
name="text">
|
||||
</td>
|
||||
<td class="text-right form-inline">
|
||||
<input type="text" class="form-control input-sm" placeholder="{% trans "Value" %}" name="value">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="fa fa-plus"></span>
|
||||
</button>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</tfoot>
|
||||
</tr>
|
||||
</tfoot>
|
||||
{% endif %}
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -15,10 +15,11 @@
|
||||
or you can manually issue gift cards.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<a href="{% url "control:organizer.giftcard.add" organizer=request.organizer.slug %}"
|
||||
class="btn btn-default btn-lg"><i class="fa fa-plus"></i> {% trans "Manually issue a gift card" %}
|
||||
</a>
|
||||
{% if "organizer.giftcards:write" in request.orgapermset %}
|
||||
<a href="{% url "control:organizer.giftcard.add" organizer=request.organizer.slug %}"
|
||||
class="btn btn-default btn-lg"><i class="fa fa-plus"></i> {% trans "Manually issue a gift card" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="panel panel-default">
|
||||
@@ -45,10 +46,12 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<p>
|
||||
<a href="{% url "control:organizer.giftcard.add" organizer=request.organizer.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Manually issue a gift card" %}</a>
|
||||
</p>
|
||||
{% if "organizer.giftcards:write" in request.orgapermset %}
|
||||
<p>
|
||||
<a href="{% url "control:organizer.giftcard.add" organizer=request.organizer.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Manually issue a gift card" %}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user