forked from CGM_Public/pretix_original
Compare commits
554 Commits
release/20
...
release/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55b522a753 | ||
|
|
0f44a2ad4e | ||
|
|
a692940397 | ||
|
|
7f2ec51c64 | ||
|
|
aba59a391c | ||
|
|
a819b8bb71 | ||
|
|
8a3b18fbd2 | ||
|
|
dd444299f0 | ||
|
|
3ee5e9cfbc | ||
|
|
f660f35766 | ||
|
|
42e26738e5 | ||
|
|
7c43f115b2 | ||
|
|
f055a598ce | ||
|
|
9138464896 | ||
|
|
479f51a84c | ||
|
|
a3ac54d419 | ||
|
|
b2841e5c61 | ||
|
|
3009f50d51 | ||
|
|
ff3a49ab2a | ||
|
|
19f3fbc7e8 | ||
|
|
bb9b9ac9aa | ||
|
|
d7f6befb5b | ||
|
|
2287be2009 | ||
|
|
0480b6873d | ||
|
|
711f08c9e8 | ||
|
|
d18914fcca | ||
|
|
2411144262 | ||
|
|
2f02d35a52 | ||
|
|
71e82fda81 | ||
|
|
ca3802da90 | ||
|
|
2c68b9e895 | ||
|
|
01092498f4 | ||
|
|
fd841ed66d | ||
|
|
04cbccb536 | ||
|
|
b8ea93de1e | ||
|
|
c49f42301c | ||
|
|
2ae0a16e67 | ||
|
|
6b06fdf822 | ||
|
|
ea3f4e5f62 | ||
|
|
d71c23f7e0 | ||
|
|
5ed7b0032b | ||
|
|
a77f2d01a7 | ||
|
|
ca4f511cde | ||
|
|
83b1c2ea7e | ||
|
|
c91eb2e20d | ||
|
|
bfb480a288 | ||
|
|
22e2143623 | ||
|
|
9e61f7f978 | ||
|
|
092de9e3c4 | ||
|
|
f0822d3c27 | ||
|
|
6fc47ca3b6 | ||
|
|
3716a686f5 | ||
|
|
9c0c77958e | ||
|
|
6154a44274 | ||
|
|
d5aff10297 | ||
|
|
846e39a652 | ||
|
|
75d37b2a37 | ||
|
|
2a0c3da8c4 | ||
|
|
fb7f4d1160 | ||
|
|
5c8817f0c3 | ||
|
|
7663bf7994 | ||
|
|
ea8f74f8aa | ||
|
|
7b34701449 | ||
|
|
06f4bfea24 | ||
|
|
e2862a98a0 | ||
|
|
82f4feadc3 | ||
|
|
906222c7d3 | ||
|
|
7ce2089ca8 | ||
|
|
2133584ed2 | ||
|
|
c3c50d7205 | ||
|
|
4f7a41a4b2 | ||
|
|
93fd79ab33 | ||
|
|
59595c2f10 | ||
|
|
91c6d09f0b | ||
|
|
570a818129 | ||
|
|
d63e2ebe2d | ||
|
|
884c97d62a | ||
|
|
032e958a00 | ||
|
|
7b16dfefbc | ||
|
|
8a5b13dee9 | ||
|
|
d7dde8c23e | ||
|
|
1e2f93fbc5 | ||
|
|
493fc03686 | ||
|
|
8d6d885f6e | ||
|
|
2aa989c293 | ||
|
|
06b226f40f | ||
|
|
73038b0d97 | ||
|
|
4513e31f0d | ||
|
|
d34175114b | ||
|
|
b2c71b47ce | ||
|
|
a889abc52b | ||
|
|
8c01b2a469 | ||
|
|
720c7fd7bb | ||
|
|
6ae6eba4de | ||
|
|
a173e347ea | ||
|
|
94d13e4cdd | ||
|
|
e618441231 | ||
|
|
cd57f1f024 | ||
|
|
075b9c187f | ||
|
|
d9f46cb817 | ||
|
|
2892d16861 | ||
|
|
9128624d68 | ||
|
|
d2cf8f801d | ||
|
|
682d0f886d | ||
|
|
d2cbd41a19 | ||
|
|
828f4e3168 | ||
|
|
e691afdd34 | ||
|
|
add90b08ec | ||
|
|
4539c6523b | ||
|
|
3453818c16 | ||
|
|
e5725d6d33 | ||
|
|
0cda1aeaaf | ||
|
|
769c451bcb | ||
|
|
5b60928205 | ||
|
|
b05f3a449d | ||
|
|
3262f2ba84 | ||
|
|
fe9d8e58a1 | ||
|
|
6706dbb4db | ||
|
|
87632ff8c5 | ||
|
|
2278dbdc4a | ||
|
|
e4e6cc0fcc | ||
|
|
ea73977c37 | ||
|
|
4fb5c6bef0 | ||
|
|
95511b0330 | ||
|
|
3340599aec | ||
|
|
e9a52d07d1 | ||
|
|
0a00a35ab1 | ||
|
|
24d8dc6c76 | ||
|
|
a9d48eaafe | ||
|
|
bb959fa494 | ||
|
|
6126309429 | ||
|
|
29b49ca82f | ||
|
|
7b7b83cb3e | ||
|
|
6bb2b3425d | ||
|
|
182d30ffb7 | ||
|
|
844c291575 | ||
|
|
ecdb1a8e09 | ||
|
|
6c6e5d5af6 | ||
|
|
c33853173b | ||
|
|
5f90bf80b8 | ||
|
|
0ba246846b | ||
|
|
88ea04551e | ||
|
|
2bfd8e17b0 | ||
|
|
1379e5f723 | ||
|
|
9fcaa3d730 | ||
|
|
22d92025c9 | ||
|
|
2b84c8d7a7 | ||
|
|
25ba2f1145 | ||
|
|
ae8ff60964 | ||
|
|
9fe6916ab5 | ||
|
|
634263f1ba | ||
|
|
67265e94a0 | ||
|
|
0fa2e9b5dd | ||
|
|
c99d93a078 | ||
|
|
9e20fac0da | ||
|
|
3e4ccc53be | ||
|
|
ce88dfa530 | ||
|
|
f0a06cd9fe | ||
|
|
7672e6274d | ||
|
|
061f578b29 | ||
|
|
79f8501a09 | ||
|
|
c5237b5021 | ||
|
|
0d6f7e74a3 | ||
|
|
21bd4a86a7 | ||
|
|
750f641018 | ||
|
|
2a385d14c4 | ||
|
|
6a7ab1bdf5 | ||
|
|
a73c4ad937 | ||
|
|
043e2eb9cf | ||
|
|
c0fb93ea3b | ||
|
|
4f9297e7d8 | ||
|
|
70b48fdd4b | ||
|
|
e7b5317431 | ||
|
|
63ef2e70e2 | ||
|
|
c4db2a48b6 | ||
|
|
de255b021e | ||
|
|
d3fce71b7f | ||
|
|
37dea068ce | ||
|
|
b5ef49cd3c | ||
|
|
245c5972c6 | ||
|
|
6597977752 | ||
|
|
580137577e | ||
|
|
a09550ce02 | ||
|
|
1b444b780d | ||
|
|
f41d8bb761 | ||
|
|
365dbe7a14 | ||
|
|
bf6078efb6 | ||
|
|
695b9a2ed6 | ||
|
|
25a15069ed | ||
|
|
11a4ea7b77 | ||
|
|
4e5e7df201 | ||
|
|
8f515aa327 | ||
|
|
dbc7fda2f8 | ||
|
|
5e686674ae | ||
|
|
5660cd7f93 | ||
|
|
a7a33ed165 | ||
|
|
9ffdf979f4 | ||
|
|
7338381e58 | ||
|
|
ce06672334 | ||
|
|
223f095611 | ||
|
|
b625dc9ec8 | ||
|
|
50c4a1c376 | ||
|
|
6fd2e42426 | ||
|
|
da651df4f0 | ||
|
|
a6f527e32d | ||
|
|
3ea61fbd1f | ||
|
|
6a959d4220 | ||
|
|
92959dbb1f | ||
|
|
6d6f3c4af8 | ||
|
|
8d14a285ca | ||
|
|
a6b8cd8a54 | ||
|
|
cb95cdc6ce | ||
|
|
cc7b00e206 | ||
|
|
136c54b9a8 | ||
|
|
28ba434e45 | ||
|
|
09320093ad | ||
|
|
89cfab6cad | ||
|
|
a180ce4c51 | ||
|
|
1200274ebf | ||
|
|
877401d8c0 | ||
|
|
44170c1b93 | ||
|
|
ce34bd0a13 | ||
|
|
b58f05efd0 | ||
|
|
3ac70e6e3a | ||
|
|
90123d6a58 | ||
|
|
48a3984db6 | ||
|
|
fe6ee4437f | ||
|
|
f470389cd8 | ||
|
|
c848594c21 | ||
|
|
e9a95b0b09 | ||
|
|
3b48b0782d | ||
|
|
b55bd8f75a | ||
|
|
39caadb335 | ||
|
|
dd6ebd7a48 | ||
|
|
ab576bb643 | ||
|
|
8bc16af36e | ||
|
|
537044bdc8 | ||
|
|
1ac7d03bb8 | ||
|
|
b939fad1c0 | ||
|
|
fb3a608c54 | ||
|
|
ebda10542e | ||
|
|
93dd6bf34d | ||
|
|
52148ebb7a | ||
|
|
82b4fe2733 | ||
|
|
79750e4f4b | ||
|
|
dbabbf7aab | ||
|
|
6158a1f2a4 | ||
|
|
68f6f921b5 | ||
|
|
d0e672435a | ||
|
|
5bc622bcfe | ||
|
|
d0184c1f48 | ||
|
|
e28bbb7ea0 | ||
|
|
fe54a42fc7 | ||
|
|
7365f165ad | ||
|
|
90ce802a33 | ||
|
|
d463878514 | ||
|
|
dd7ee84d29 | ||
|
|
f1e2d1f44c | ||
|
|
87a6a58f32 | ||
|
|
b2dd56bd41 | ||
|
|
f0ceab2305 | ||
|
|
14e3316dd9 | ||
|
|
eb28fdcba9 | ||
|
|
49e4a0faa0 | ||
|
|
e486089590 | ||
|
|
4bb02d4ad9 | ||
|
|
8ad852d9cb | ||
|
|
868fcfc471 | ||
|
|
d847c9a095 | ||
|
|
d1c6b22624 | ||
|
|
842987f48e | ||
|
|
58fc13ed91 | ||
|
|
8010d2e6bb | ||
|
|
1566f54764 | ||
|
|
9d380557e1 | ||
|
|
5758e0dd68 | ||
|
|
b4629e24a5 | ||
|
|
27f5121211 | ||
|
|
a5007e4bd6 | ||
|
|
fb3046210b | ||
|
|
37908bd042 | ||
|
|
74bcbe8f07 | ||
|
|
29f378c58b | ||
|
|
9b537aeb5c | ||
|
|
4b5cd35a0e | ||
|
|
e0675233d5 | ||
|
|
e9726a5227 | ||
|
|
78b65d0757 | ||
|
|
ef620ceb37 | ||
|
|
8575a5f1cd | ||
|
|
5762ffc035 | ||
|
|
79286bb051 | ||
|
|
05a2f411db | ||
|
|
e93e5c047c | ||
|
|
2619a658c9 | ||
|
|
808775c76b | ||
|
|
9f297fbd25 | ||
|
|
d882da0adb | ||
|
|
73bd4a746e | ||
|
|
bc5d0763f3 | ||
|
|
ff084f04b1 | ||
|
|
71af40a08b | ||
|
|
ef60093bae | ||
|
|
49c4cc639f | ||
|
|
e2800019f6 | ||
|
|
c44ea8aa81 | ||
|
|
47a03e1b2a | ||
|
|
86ddca15ca | ||
|
|
294b3966b0 | ||
|
|
fecc00231b | ||
|
|
b3dfc459f5 | ||
|
|
e21d63a7be | ||
|
|
9a807df158 | ||
|
|
e95d551711 | ||
|
|
7188e44fe5 | ||
|
|
a6a93555b6 | ||
|
|
94eb473e42 | ||
|
|
cbc3a344c1 | ||
|
|
47db52d75f | ||
|
|
ba99fe597c | ||
|
|
b638c00952 | ||
|
|
bfcca7046a | ||
|
|
ad5d10ff67 | ||
|
|
54d327deea | ||
|
|
d6505f946f | ||
|
|
9c4efa7dcf | ||
|
|
e6d26c4962 | ||
|
|
7ddbbe21f7 | ||
|
|
8d5ad0bd9e | ||
|
|
aff6a6f022 | ||
|
|
46008818ce | ||
|
|
95db04bad2 | ||
|
|
d0c62ec1cf | ||
|
|
d6cbb130bd | ||
|
|
097d2fcda0 | ||
|
|
41a7c13970 | ||
|
|
1b725810dd | ||
|
|
251f486480 | ||
|
|
a7afcdf753 | ||
|
|
0722341073 | ||
|
|
207bf101b8 | ||
|
|
e8f7cea1bf | ||
|
|
aa55eb2de2 | ||
|
|
9dc5c1b266 | ||
|
|
514f1def4d | ||
|
|
c2bc97a0d8 | ||
|
|
7fba473426 | ||
|
|
be87ba0000 | ||
|
|
76b7643c39 | ||
|
|
b1a3963b33 | ||
|
|
586e694ff3 | ||
|
|
f4383c67a4 | ||
|
|
46b2214836 | ||
|
|
0e20d897d2 | ||
|
|
0c09cccd4f | ||
|
|
5ca0833db1 | ||
|
|
7a63498333 | ||
|
|
b8c0887f79 | ||
|
|
9da65f60d7 | ||
|
|
806124304a | ||
|
|
0d57673a47 | ||
|
|
166b5e4f3b | ||
|
|
541b8f5bd6 | ||
|
|
d2b96b2425 | ||
|
|
04d4c4f8f1 | ||
|
|
f4da94cbcd | ||
|
|
97e3d5387f | ||
|
|
8fc07523a9 | ||
|
|
f18b0ae187 | ||
|
|
f7e16f56ac | ||
|
|
3f4e869cea | ||
|
|
8c2a1d58f4 | ||
|
|
0b05eb34f4 | ||
|
|
be48c5f94c | ||
|
|
cebb6d3b43 | ||
|
|
0de96ed066 | ||
|
|
a9d506b1fa | ||
|
|
7a01057429 | ||
|
|
64e1a602d6 | ||
|
|
fe060c387a | ||
|
|
1dba4c7cc9 | ||
|
|
20b2a3d2aa | ||
|
|
044f0c5480 | ||
|
|
4d394f9e8a | ||
|
|
247c4c6c9c | ||
|
|
11a038feb3 | ||
|
|
9d57ea8534 | ||
|
|
189c77207f | ||
|
|
3422003a9c | ||
|
|
8da38ba99d | ||
|
|
fc05208b92 | ||
|
|
b163109c56 | ||
|
|
3ba818336e | ||
|
|
8aecf4f98f | ||
|
|
42f3ca9661 | ||
|
|
f7b405b210 | ||
|
|
11a6390cfc | ||
|
|
239a7746df | ||
|
|
03701eaa82 | ||
|
|
356f215d8e | ||
|
|
7962c4e380 | ||
|
|
a59711ed32 | ||
|
|
49370a5e08 | ||
|
|
980aec7326 | ||
|
|
44294110fe | ||
|
|
ce1078a783 | ||
|
|
0e0cede0ee | ||
|
|
5c833cd493 | ||
|
|
64d6a34039 | ||
|
|
cf380069b4 | ||
|
|
48168a4c68 | ||
|
|
6482fe79b0 | ||
|
|
0f696f42f6 | ||
|
|
79d59553d7 | ||
|
|
cc903c39f0 | ||
|
|
b6a42ac8d2 | ||
|
|
5f5001edb5 | ||
|
|
fb403dad88 | ||
|
|
a73c8f580d | ||
|
|
f490c89e98 | ||
|
|
159658ae46 | ||
|
|
595aff0579 | ||
|
|
b2842ec3a0 | ||
|
|
f09f07ec7c | ||
|
|
cff073f0d6 | ||
|
|
eb9d0c6cf9 | ||
|
|
e263946c3f | ||
|
|
7ee957cff0 | ||
|
|
ac2fe4b62d | ||
|
|
577e276df3 | ||
|
|
11956a8f4d | ||
|
|
d0c58713c4 | ||
|
|
46da0bda61 | ||
|
|
009e3a6d36 | ||
|
|
c01855270b | ||
|
|
2df1585b71 | ||
|
|
bf48ae567f | ||
|
|
5a72c72d18 | ||
|
|
ac02f3b417 | ||
|
|
58add74b3a | ||
|
|
0067c3537d | ||
|
|
64ae1d08a6 | ||
|
|
ca25c3c81e | ||
|
|
abbe9ec897 | ||
|
|
a7735d5d9e | ||
|
|
174c81a22b | ||
|
|
38c6294ede | ||
|
|
217ae90642 | ||
|
|
0c998ca884 | ||
|
|
b1691f867d | ||
|
|
72e451b27b | ||
|
|
8124ced6c1 | ||
|
|
a3139944f6 | ||
|
|
48493c517b | ||
|
|
535a29bf4b | ||
|
|
9d415f5179 | ||
|
|
990e9da21d | ||
|
|
4afb7a4976 | ||
|
|
22e5579ed1 | ||
|
|
79cd84e243 | ||
|
|
fad4b8846c | ||
|
|
0f4790afd8 | ||
|
|
2068a5ac29 | ||
|
|
440c97061c | ||
|
|
3b6d0c4341 | ||
|
|
06ac4b0250 | ||
|
|
a233b92f6f | ||
|
|
4ea4189e6d | ||
|
|
50838b9cea | ||
|
|
c68ee56d51 | ||
|
|
5c0587c30e | ||
|
|
f3f42a8a42 | ||
|
|
20d0a9a0ed | ||
|
|
cda8144ff0 | ||
|
|
43e8875c1e | ||
|
|
28c142b2ed | ||
|
|
46203fd8ba | ||
|
|
52e45c37df | ||
|
|
d1580dca2c | ||
|
|
cd9e672871 | ||
|
|
427f508627 | ||
|
|
887d06a485 | ||
|
|
fb49046ac1 | ||
|
|
ce826e50f7 | ||
|
|
d866c6954d | ||
|
|
40c76dda74 | ||
|
|
f532853021 | ||
|
|
8cb187502d | ||
|
|
156037f2cd | ||
|
|
134d63fb3f | ||
|
|
816002fda0 | ||
|
|
3939bbc11c | ||
|
|
95d1603cc7 | ||
|
|
ada3ada699 | ||
|
|
97eaeac4f2 | ||
|
|
d67f5c650c | ||
|
|
273c1ae0a6 | ||
|
|
a946c10ab4 | ||
|
|
2d8fba7d7c | ||
|
|
e4e0bd7ca0 | ||
|
|
3651c88289 | ||
|
|
c92ca40026 | ||
|
|
4d00efb549 | ||
|
|
7e60d13910 | ||
|
|
35800e21c7 | ||
|
|
99b4c5bd36 | ||
|
|
f121205dd1 | ||
|
|
1ac54cd209 | ||
|
|
4694719a53 | ||
|
|
9513b6e8d7 | ||
|
|
4fd7d406a0 | ||
|
|
47cb5b207a | ||
|
|
7d2cf68727 | ||
|
|
459cb47ca8 | ||
|
|
39705556cd | ||
|
|
9f794290dc | ||
|
|
b6221ab6d9 | ||
|
|
483518bce9 | ||
|
|
d9019ae735 | ||
|
|
721fd3b998 | ||
|
|
ad0d3f5469 | ||
|
|
40b44f9272 | ||
|
|
304d290f22 | ||
|
|
7592a8a575 | ||
|
|
4f33159f93 | ||
|
|
819ce6abf7 | ||
|
|
760dfd22b8 | ||
|
|
f9eaa193c9 | ||
|
|
c7720a2553 | ||
|
|
7754f5420c | ||
|
|
2c7ada6e86 | ||
|
|
3f31843fd1 | ||
|
|
952b9bd9b9 | ||
|
|
3619a6bcd0 | ||
|
|
3e2c12cdb0 | ||
|
|
a3ce3b9af3 | ||
|
|
b6461e9303 | ||
|
|
f7dfd51c2c | ||
|
|
3b98d87a26 | ||
|
|
f045062055 | ||
|
|
eb501dd1ea | ||
|
|
2d8793c355 | ||
|
|
d32bd717b7 | ||
|
|
6e6b75d55e | ||
|
|
50b5f760bb | ||
|
|
9ab2e61c31 | ||
|
|
4876a0b61f | ||
|
|
56bbcb65c3 | ||
|
|
5bb1cb498f | ||
|
|
6bf23b0fdd | ||
|
|
5deb1a8c69 | ||
|
|
1523137300 | ||
|
|
04ef097eb1 | ||
|
|
a5d4434a64 |
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -26,12 +26,12 @@ jobs:
|
||||
matrix:
|
||||
python-version: ["3.11"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
|
||||
6
.github/workflows/docs.yml
vendored
6
.github/workflows/docs.yml
vendored
@@ -25,12 +25,12 @@ jobs:
|
||||
name: Spellcheck
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
|
||||
12
.github/workflows/strings.yml
vendored
12
.github/workflows/strings.yml
vendored
@@ -23,12 +23,12 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
name: Check gettext syntax
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
@@ -48,12 +48,12 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
name: Spellcheck
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
|
||||
16
.github/workflows/style.yml
vendored
16
.github/workflows/style.yml
vendored
@@ -23,12 +23,12 @@ jobs:
|
||||
name: isort
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
@@ -43,12 +43,12 @@ jobs:
|
||||
name: flake8
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
@@ -63,9 +63,9 @@ jobs:
|
||||
name: licenseheaders
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
- name: Install Dependencies
|
||||
|
||||
7
.github/workflows/tests.yml
vendored
7
.github/workflows/tests.yml
vendored
@@ -5,7 +5,6 @@ on:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- 'doc/**'
|
||||
- 'src/pretix/locale/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
@@ -32,7 +31,7 @@ jobs:
|
||||
- database: sqlite
|
||||
python-version: "3.10"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: harmon758/postgresql-action@v1
|
||||
with:
|
||||
postgresql version: '15'
|
||||
@@ -41,10 +40,10 @@ jobs:
|
||||
postgresql password: 'postgres'
|
||||
if: matrix.database == 'postgres'
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
before_script:
|
||||
tests:
|
||||
image:
|
||||
name: pretix/ci-image
|
||||
stage: test
|
||||
before_script:
|
||||
- pip install -U pip uv
|
||||
- uv pip install --system -U wheel setuptools
|
||||
script:
|
||||
- virtualenv env
|
||||
- source env/bin/activate
|
||||
- pip install -U pip wheel setuptools
|
||||
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
|
||||
- uv pip install --system -e ".[dev]"
|
||||
- cd src
|
||||
- python manage.py check
|
||||
- make all compress
|
||||
- py.test --reruns 3 -n 3 tests
|
||||
tags:
|
||||
- python3
|
||||
- PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg py.test --reruns 3 -n 3 tests --maxfail=100
|
||||
except:
|
||||
- pypi
|
||||
pypi:
|
||||
stage: release
|
||||
image:
|
||||
name: pretix/ci-image
|
||||
before_script:
|
||||
- cat $PYPIRC > ~/.pypirc
|
||||
- pip install -U pip uv
|
||||
- uv pip install --system -U wheel setuptools twine build pretix-plugin-build check-manifest
|
||||
script:
|
||||
- cp /keys/.pypirc ~/.pypirc
|
||||
- virtualenv env
|
||||
- source env/bin/activate
|
||||
- pip install -U pip wheel setuptools check-manifest twine
|
||||
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
|
||||
- uv pip install --system -e ".[dev]"
|
||||
- python setup.py sdist
|
||||
- pip install dist/pretix-*.tar.gz
|
||||
- uv pip install --system dist/pretix-*.tar.gz
|
||||
- python -m pretix migrate
|
||||
- python -m pretix check
|
||||
- cd src
|
||||
@@ -33,13 +34,12 @@ pypi:
|
||||
- python -m build
|
||||
- twine check dist/*
|
||||
- twine upload dist/*
|
||||
tags:
|
||||
- python3
|
||||
only:
|
||||
- pypi
|
||||
artifacts:
|
||||
paths:
|
||||
- src/dist/
|
||||
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
|
||||
@@ -47,7 +47,7 @@ if [ "$1" == "taskworker" ]; then
|
||||
fi
|
||||
|
||||
if [ "$1" == "upgrade" ]; then
|
||||
exec python3 -m pretix updatestyles
|
||||
exec python3 -m pretix updateassets
|
||||
fi
|
||||
|
||||
exec python3 -m pretix "$@"
|
||||
|
||||
@@ -52,10 +52,18 @@ Example::
|
||||
``currency``
|
||||
The default currency as a three-letter code. Defaults to ``EUR``.
|
||||
|
||||
``cachedir``
|
||||
The local path to a directory where temporary files will be stored.
|
||||
Defaults to the ``cache`` directory below the ``datadir``.
|
||||
|
||||
``datadir``
|
||||
The local path to a data directory that will be used for storing user uploads and similar
|
||||
data. Defaults to the value of the environment variable ``DATA_DIR`` or ``data``.
|
||||
|
||||
``logdir``
|
||||
The local path to a directory where log files will be stored.
|
||||
Defaults to the ``logs`` directory below the ``datadir``.
|
||||
|
||||
``plugins_default``
|
||||
A comma-separated list of plugins that are enabled by default for all new events.
|
||||
Defaults to ``pretix.plugins.sendmail,pretix.plugins.statistics``.
|
||||
@@ -89,8 +97,9 @@ Example::
|
||||
Defaults to ``off``.
|
||||
|
||||
``obligatory_2fa``
|
||||
Enables or disables obligatory usage of Two-Factor Authentication for users of the pretix backend.
|
||||
Defaults to ``False``
|
||||
Enables or disables obligatory usage of two-factor authentication for users of the pretix backend.
|
||||
Can be ``True`` to make two-factor authentication obligatory for all users or ``staff`` to make it only
|
||||
obligatory to users with admin permissions. Defaults to ``False``.
|
||||
|
||||
``trust_x_forwarded_for``
|
||||
Specifies whether the ``X-Forwarded-For`` header can be trusted. Only set to ``on`` if you have a reverse
|
||||
@@ -149,6 +158,7 @@ Example::
|
||||
host=localhost
|
||||
port=3306
|
||||
advisory_lock_index=1
|
||||
disable_server_side_cursors=0
|
||||
sslmode=require
|
||||
sslrootcert=/etc/pretix/postgresql-ca.crt
|
||||
sslcert=/etc/pretix/postgresql-client-crt.crt
|
||||
@@ -169,6 +179,11 @@ Example::
|
||||
and are not scoped to a specific database. If you run multiple pretix applications with the same PostgreSQL server,
|
||||
you should set separate values for this setting (integers up to 256).
|
||||
|
||||
``disable_server_side_cursors``
|
||||
On PostgreSQL pretix might use server side cursors for certain operations. This is generally fine but will break in
|
||||
specific circumstances, for example when connecting to PostgreSQL through a PGBouncer configured with a transaction
|
||||
pool mode. Off by default (i.e. by default server side cursors will be used).
|
||||
|
||||
``sslmode``, ``sslrootcert``
|
||||
Connection TLS details for the PostgreSQL database connection. Possible values of ``sslmode`` are ``disable``, ``allow``, ``prefer``, ``require``, ``verify-ca``, and ``verify-full``. ``sslrootcert`` should be the accessible path of the ca certificate. Both values are empty by default.
|
||||
|
||||
@@ -345,7 +360,7 @@ to speed up various operations::
|
||||
The location of redis, as a URL of the form ``redis://[:password]@localhost:6379/0``
|
||||
or ``unix://[:password]@/path/to/socket.sock?db=0``
|
||||
|
||||
``session``
|
||||
``sessions``
|
||||
When this is set to ``True``, redis will be used as the session storage.
|
||||
|
||||
``sentinels``
|
||||
@@ -521,4 +536,4 @@ pretix can optionally make use of a GeoIP database for some features. It needs a
|
||||
|
||||
|
||||
.. _GeoAcumen: https://github.com/geoacumen/geoacumen-country
|
||||
.. _GeoLite2: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data
|
||||
.. _GeoLite2: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data
|
||||
|
||||
18
doc/admin/installation/community.rst
Normal file
18
doc/admin/installation/community.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
.. highlight:: none
|
||||
|
||||
.. _`community`:
|
||||
|
||||
Community install guides
|
||||
========================
|
||||
|
||||
.. warning:: The guides are maintained by the community and not by the pretix core team. If you encounter any issues with the guides, please report them to the maintainers of the guides. The pretix core team can not provide support for installs using these guides.
|
||||
|
||||
Kubernetes
|
||||
----------
|
||||
|
||||
- Helm Chart by techwolf12 - A Helm chart for deploying pretix on Kubernetes. The chart documentation is available on `ArtifactHub <https://artifacthub.io/packages/helm/techwolf12/pretix>`_ and the source code is available on `GitHub <https://github.com/Techwolf12/charts/tree/main/pretix-helm>`_.
|
||||
|
||||
Docker
|
||||
------
|
||||
|
||||
- `docker compose setup <https://github.com/ZPascal/pretix-docker-compose>`_ by ZPascal
|
||||
@@ -19,7 +19,7 @@ You can use ``pip`` to update pretix directly to the development branch. Then, u
|
||||
(venv)$ pip3 install -U "git+https://github.com/pretix/pretix.git#egg=pretix"
|
||||
(venv)$ python -m pretix migrate
|
||||
(venv)$ python -m pretix rebuild
|
||||
(venv)$ python -m pretix updatestyles
|
||||
(venv)$ python -m pretix updateassets
|
||||
# systemctl restart pretix-web pretix-worker
|
||||
|
||||
Docker installation
|
||||
|
||||
@@ -14,3 +14,4 @@ for your needs.
|
||||
manual_smallscale
|
||||
dev_version
|
||||
enterprise
|
||||
community
|
||||
|
||||
@@ -65,7 +65,7 @@ Package dependencies
|
||||
To build and run pretix, you will need the following debian packages::
|
||||
|
||||
# apt-get install git build-essential python3-dev python3-venv python3 python3-pip \
|
||||
python3-dev libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
|
||||
libxml2-dev libxslt1-dev libffi-dev zlib1g-dev libssl-dev \
|
||||
gettext libpq-dev libjpeg-dev libopenjp2-7-dev
|
||||
|
||||
Config file
|
||||
@@ -120,6 +120,7 @@ Now we will install pretix itself. The following steps are to be executed as the
|
||||
actually install pretix, we will create a virtual environment to isolate the python packages from your global
|
||||
python installation::
|
||||
|
||||
# sudo -u pretix -s
|
||||
$ python3 -m venv /var/pretix/venv
|
||||
$ source /var/pretix/venv/bin/activate
|
||||
(venv)$ pip3 install -U pip setuptools wheel
|
||||
@@ -279,11 +280,12 @@ Updates
|
||||
|
||||
To upgrade to a new pretix release, pull the latest code changes and run the following commands::
|
||||
|
||||
# sudo -u pretix -s
|
||||
$ source /var/pretix/venv/bin/activate
|
||||
(venv)$ pip3 install -U --upgrade-strategy eager pretix gunicorn
|
||||
(venv)$ python -m pretix migrate
|
||||
(venv)$ python -m pretix rebuild
|
||||
(venv)$ python -m pretix updatestyles
|
||||
(venv)$ python -m pretix updateassets
|
||||
# systemctl restart pretix-web pretix-worker
|
||||
|
||||
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to. Pay special
|
||||
@@ -323,7 +325,7 @@ Then, proceed like after any plugin installation::
|
||||
|
||||
(venv)$ python -m pretix migrate
|
||||
(venv)$ python -m pretix rebuild
|
||||
(venv)$ python -m pretix updatestyles
|
||||
(venv)$ python -m pretix updateassets
|
||||
# systemctl restart pretix-web pretix-worker
|
||||
|
||||
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-22-04
|
||||
|
||||
@@ -103,6 +103,12 @@ pretix_celery_tasks_queued_count
|
||||
pretix_celery_tasks_queued_age_seconds
|
||||
The age of the longest-waiting in the worker queue in seconds, labeled with ``queue``.
|
||||
|
||||
pretix_logins_successful
|
||||
Counter. The number of successful backend logins.
|
||||
|
||||
pretix_logins_failed
|
||||
Counter. The number of failed backend logins, labeled with ``reason``.
|
||||
|
||||
.. _metric types: https://prometheus.io/docs/concepts/metric_types/
|
||||
.. _Prometheus: https://prometheus.io/
|
||||
.. _cProfile: https://docs.python.org/3/library/profile.html
|
||||
|
||||
@@ -73,4 +73,11 @@ This release includes a migration that changes retroactively fills an `organizer
|
||||
`pretixbase_logentry`. If you have a large database, the migration step of the upgrade might take significantly
|
||||
longer than usual, so plan the update accordingly.
|
||||
|
||||
Upgrade to 2024.7.0 or newer
|
||||
"""""""""""""""""""""""""""""
|
||||
|
||||
This release includes a migration that changes how sales channels are referred on orders.
|
||||
If you have a large database, the migration step of the upgrade might take significantly longer than usual, so plan
|
||||
the update accordingly.
|
||||
|
||||
.. _blog: https://pretix.eu/about/en/blog/
|
||||
|
||||
@@ -249,7 +249,10 @@ You can get three response codes:
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"event": "democon",
|
||||
"event": {
|
||||
"name": "Demo Conference",
|
||||
"slug": "democon"
|
||||
},
|
||||
"subevent": 23,
|
||||
"checkinlist": 5
|
||||
}
|
||||
|
||||
@@ -94,7 +94,9 @@ If you want the user to return to your application after the payment is complete
|
||||
"Plugins". Enable the plugin "Redirection from order page". Then, go to the new page "Settings", then "Redirection".
|
||||
Enter the base URL of your web application. This will allow you to redirect to pages under this base URL later on.
|
||||
For example, if you want users to be redirected to ``https://example.org/order/return?tx_id=1234``, you could now
|
||||
either enter ``https://example.org`` or ``https://example.org/order/``.
|
||||
either enter ``https://example.org/order/`` or ``https://example.org/``.
|
||||
Please note that in the latter case the trailing slash is required, ``https://example.org`` is not allowed to prevent.
|
||||
Only base URLs with a secure (``https://``) or local (``http://localhost``) origin are permitted.
|
||||
|
||||
The user will be redirected back to your page instead of pretix' order confirmation page after the payment,
|
||||
**regardless of whether it was successful or not**. We will append an ``error=…`` query parameter with an error
|
||||
|
||||
@@ -40,6 +40,11 @@ answers list of objects Answers to user
|
||||
seat objects The assigned seat (or ``null``)
|
||||
├ id integer Internal ID of the seat instance
|
||||
├ name string Human-readable seat name
|
||||
├ zone_name string Name of the zone the seat is in
|
||||
├ row_name string Name/number of the row the seat is in
|
||||
├ row_label string Additional label of the row (or ``null``)
|
||||
├ seat_number string Number of the seat within the row
|
||||
├ seat_label string Additional label of the seat (or ``null``)
|
||||
└ seat_guid string Identifier of the seat within the seating plan
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ external_identifier string External ID of
|
||||
the API, but is read-only for customers created through a
|
||||
SSO integration.
|
||||
email string Customer email address
|
||||
phone string Customer phone number
|
||||
name string Name of this customer (or ``null``)
|
||||
name_parts object of strings Decomposition of name (i.e. given name, family name)
|
||||
is_active boolean Whether this account is active
|
||||
@@ -39,6 +40,10 @@ password string Can only be set
|
||||
|
||||
Passwords can now be set through the API during customer creation.
|
||||
|
||||
.. versionchanged:: 2024.3
|
||||
|
||||
The attribute ``phone`` has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -71,6 +76,7 @@ Endpoints
|
||||
"identifier": "8WSAJCJ",
|
||||
"external_identifier": null,
|
||||
"email": "customer@example.org",
|
||||
"phone": "+493012345678",
|
||||
"name": "John Doe",
|
||||
"name_parts": {
|
||||
"_scheme": "full",
|
||||
@@ -118,6 +124,7 @@ Endpoints
|
||||
"identifier": "8WSAJCJ",
|
||||
"external_identifier": null,
|
||||
"email": "customer@example.org",
|
||||
"phone": "+493012345678",
|
||||
"name": "John Doe",
|
||||
"name_parts": {
|
||||
"_scheme": "full",
|
||||
@@ -155,6 +162,7 @@ Endpoints
|
||||
|
||||
{
|
||||
"email": "test@example.org",
|
||||
"phone": "+493012345678",
|
||||
"password": "verysecret",
|
||||
"send_email": true
|
||||
}
|
||||
@@ -171,6 +179,7 @@ Endpoints
|
||||
"identifier": "8WSAJCJ",
|
||||
"external_identifier": null,
|
||||
"email": "test@example.org",
|
||||
"phone": "+493012345678",
|
||||
...
|
||||
}
|
||||
|
||||
@@ -215,6 +224,7 @@ Endpoints
|
||||
"identifier": "8WSAJCJ",
|
||||
"external_identifier": null,
|
||||
"email": "test@example.org",
|
||||
"phone": "+493012345678",
|
||||
…
|
||||
}
|
||||
|
||||
@@ -249,6 +259,7 @@ Endpoints
|
||||
"identifier": "8WSAJCJ",
|
||||
"external_identifier": null,
|
||||
"email": null,
|
||||
"phone": null,
|
||||
…
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,12 @@ id integer Internal ID
|
||||
active boolean The discount will be ignored if this is ``false``
|
||||
internal_name string A name for the rule used in the backend
|
||||
position integer An integer, used for sorting the rules which are applied in order
|
||||
sales_channels list of strings Sales channels this discount is available on, such as
|
||||
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
||||
all_sales_channels boolean If ``true`` (default), the discount is available on all sales channels
|
||||
that support discounts.
|
||||
limit_sales_channels list of strings List of sales channel identifiers the discount is available on
|
||||
if ``all_sales_channels`` is ``false``.
|
||||
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||
and ``limit_sales_channels`` instead.
|
||||
available_from datetime The first date time at which this discount can be applied
|
||||
(or ``null``).
|
||||
available_until datetime The last date time at which this discount can be applied
|
||||
@@ -95,6 +99,8 @@ Endpoints
|
||||
"active": true,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
@@ -151,6 +157,8 @@ Endpoints
|
||||
"active": true,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
@@ -193,6 +201,8 @@ Endpoints
|
||||
"active": true,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
@@ -224,6 +234,8 @@ Endpoints
|
||||
"active": true,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
@@ -284,6 +296,8 @@ Endpoints
|
||||
"active": false,
|
||||
"internal_name": "3 for 2",
|
||||
"position": 1,
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_until": null,
|
||||
|
||||
@@ -49,8 +49,11 @@ item_meta_properties object Item-specific m
|
||||
valid_keys object Cryptographic keys for non-default signature schemes.
|
||||
For performance reason, value is omitted in lists and
|
||||
only contained in detail views. Value can be cached.
|
||||
sales_channels list A list of sales channels this event is available for
|
||||
sale on.
|
||||
all_sales_channels boolean If ``true`` (default), the event is available on all sales channels.
|
||||
limit_sales_channels list of strings List of sales channel identifiers the event is available on
|
||||
if ``all_sales_channels`` is ``false``.
|
||||
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||
and ``limit_sales_channels`` instead.
|
||||
public_url string The public, customer-facing URL of the event (read-only).
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
@@ -131,11 +134,13 @@ Endpoints
|
||||
"pretix.plugins.paypal",
|
||||
"pretix.plugins.ticketoutputpdf"
|
||||
],
|
||||
"sales_channels": [
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": [
|
||||
"web",
|
||||
"pretixpos",
|
||||
"resellers"
|
||||
],
|
||||
"sales_channels": [],
|
||||
"public_url": "https://pretix.eu/bigevents/sampleconf/"
|
||||
}
|
||||
]
|
||||
@@ -225,6 +230,8 @@ Endpoints
|
||||
"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUNvd0JRWURLMlZ3QXlFQTdBRDcvdkZBMzNFc1k0ejJQSHI3aVpQc1o4bjVkaDBhalA4Z3l6Tm1tSXM9Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo="
|
||||
]
|
||||
},
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": [],
|
||||
"sales_channels": [
|
||||
"web",
|
||||
"pretixpos",
|
||||
@@ -282,11 +289,8 @@ Endpoints
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
],
|
||||
"sales_channels": [
|
||||
"web",
|
||||
"pretixpos",
|
||||
"resellers"
|
||||
]
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": []
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -322,6 +326,8 @@ Endpoints
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
],
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": [],
|
||||
"sales_channels": [
|
||||
"web",
|
||||
"pretixpos",
|
||||
@@ -387,11 +393,8 @@ Endpoints
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
],
|
||||
"sales_channels": [
|
||||
"web",
|
||||
"pretixpos",
|
||||
"resellers"
|
||||
]
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": []
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -427,6 +430,8 @@ Endpoints
|
||||
"pretix.plugins.stripe",
|
||||
"pretix.plugins.paypal"
|
||||
],
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": [],
|
||||
"sales_channels": [
|
||||
"web",
|
||||
"pretixpos",
|
||||
@@ -502,6 +507,8 @@ Endpoints
|
||||
"pretix.plugins.paypal",
|
||||
"pretix.plugins.pretixdroid"
|
||||
],
|
||||
"all_sales_channels": true,
|
||||
"limit_sales_channels": [],
|
||||
"sales_channels": [
|
||||
"web",
|
||||
"pretixpos",
|
||||
|
||||
@@ -96,6 +96,8 @@ Endpoints
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query string secret: Only show gift cards with the given secret.
|
||||
:query string value: Only show gift cards with the given value.
|
||||
:query boolean expired: Filter for gift cards that are (not) expired.
|
||||
:query boolean testmode: Filter for gift cards that are (not) in test mode.
|
||||
:query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer.
|
||||
:query string expand: If you pass ``"owner_ticket"``, the respective field will be shown as a nested value instead of just an ID.
|
||||
|
||||
@@ -30,6 +30,7 @@ at :ref:`plugin-docs`.
|
||||
checkinlists
|
||||
waitinglist
|
||||
customers
|
||||
saleschannels
|
||||
membershiptypes
|
||||
memberships
|
||||
giftcards
|
||||
|
||||
@@ -38,11 +38,14 @@ require_membership boolean If ``true``, bo
|
||||
require_membership_hidden boolean If ``true`` and ``require_membership`` is set, this variation will
|
||||
be hidden from users without a valid membership.
|
||||
require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
|
||||
sales_channels list of strings Sales channels this variation is available on, such as
|
||||
``"web"`` or ``"resellers"``. Defaults to all existing sales channels.
|
||||
all_sales_channels boolean If ``true`` (default), the variation is available on all sales channels.
|
||||
limit_sales_channels list of strings List of sales channel identifiers the variation is available on
|
||||
if ``all_sales_channels`` is ``false``.
|
||||
The item-level list takes precedence, i.e. a sales
|
||||
channel needs to be on both lists for the item to be
|
||||
available.
|
||||
channel needs to be on both lists for the variation to be
|
||||
available (unless ``all_sales_channels`` is used).
|
||||
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||
and ``limit_sales_channels`` instead.
|
||||
available_from datetime The first date time at which this variation can be bought
|
||||
(or ``null``).
|
||||
available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
|
||||
@@ -111,6 +114,8 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -139,6 +144,8 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -157,6 +164,7 @@ Endpoints
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query string search: Filter the list by the value of the variation (substring search).
|
||||
:query boolean active: If set to ``true`` or ``false``, only items with this value for the field ``active`` will be
|
||||
returned.
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -202,6 +210,8 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -244,7 +254,8 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
@@ -277,6 +288,8 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -341,6 +354,8 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_hidden": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
|
||||
@@ -46,8 +46,11 @@ personalized boolean ``true`` for
|
||||
position integer An integer, used for sorting
|
||||
picture file A product picture to be displayed in the shop
|
||||
(can be ``null``).
|
||||
sales_channels list of strings Sales channels this product is available on, such as
|
||||
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
||||
all_sales_channels boolean If ``true`` (default), the item is available on all sales channels.
|
||||
limit_sales_channels list of strings List of sales channel identifiers the item is available on
|
||||
if ``all_sales_channels`` is ``false``.
|
||||
sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||
and ``limit_sales_channels`` instead.
|
||||
available_from datetime The first date time at which this item can be bought
|
||||
(or ``null``).
|
||||
available_from_mode string If ``hide`` (the default), this item is hidden in the shop
|
||||
@@ -157,11 +160,14 @@ variations list of objects A list with o
|
||||
be hidden from users without a valid membership.
|
||||
├ require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
|
||||
Markdown syntax or can be ``null``.
|
||||
├ sales_channels list of strings Sales channels this variation is available on, such as
|
||||
``"web"`` or ``"resellers"``. Defaults to all existing sales channels.
|
||||
├ all_sales_channels boolean If ``true`` (default), the variation is available on all sales channels.
|
||||
├ limit_sales_channels list of strings List of sales channel identifiers the variation is available on
|
||||
if ``all_sales_channels`` is ``false``.
|
||||
The item-level list takes precedence, i.e. a sales
|
||||
channel needs to be on both lists for the item to be
|
||||
available.
|
||||
channel needs to be on both lists for the variation to be
|
||||
available (unless ``all_sales_channels`` is used).
|
||||
├ sales_channels list of strings **DEPRECATED.** Legacy interface, use ``all_sales_channels``
|
||||
and ``limit_sales_channels`` instead.
|
||||
├ available_from datetime The first date time at which this variation can be bought
|
||||
(or ``null``).
|
||||
├ available_from_mode string If ``hide`` (the default), this variation is hidden in the shop
|
||||
@@ -276,6 +282,8 @@ Endpoints
|
||||
"id": 1,
|
||||
"name": {"en": "Standard ticket"},
|
||||
"internal_name": "",
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"default_price": "23.00",
|
||||
"original_price": null,
|
||||
@@ -340,6 +348,8 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -362,6 +372,8 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -380,6 +392,7 @@ Endpoints
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:query string search: Filter the list by internal name or name of the item (substring search).
|
||||
:query boolean active: If set to ``true`` or ``false``, only items with this value for the field ``active`` will be
|
||||
returned.
|
||||
:query integer category: If set to the ID of a category, only items within that category will be returned.
|
||||
@@ -420,6 +433,8 @@ Endpoints
|
||||
"id": 1,
|
||||
"name": {"en": "Standard ticket"},
|
||||
"internal_name": "",
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"default_price": "23.00",
|
||||
"original_price": null,
|
||||
@@ -485,6 +500,8 @@ Endpoints
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"description": null,
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -506,6 +523,8 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -545,7 +564,8 @@ Endpoints
|
||||
"id": 1,
|
||||
"name": {"en": "Standard ticket"},
|
||||
"internal_name": "",
|
||||
"sales_channels": ["web"],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"default_price": "23.00",
|
||||
"original_price": null,
|
||||
"category": null,
|
||||
@@ -608,7 +628,8 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
@@ -630,7 +651,8 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"sales_channels": ["web"],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
"available_until": null,
|
||||
@@ -657,6 +679,8 @@ Endpoints
|
||||
"id": 1,
|
||||
"name": {"en": "Standard ticket"},
|
||||
"internal_name": "",
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"default_price": "23.00",
|
||||
"original_price": null,
|
||||
@@ -721,6 +745,8 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -743,6 +769,8 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -801,6 +829,8 @@ Endpoints
|
||||
"id": 1,
|
||||
"name": {"en": "Ticket"},
|
||||
"internal_name": "",
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"default_price": "25.00",
|
||||
"original_price": null,
|
||||
@@ -865,6 +895,8 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
@@ -887,6 +919,8 @@ Endpoints
|
||||
"require_approval": false,
|
||||
"require_membership": false,
|
||||
"require_membership_types": [],
|
||||
"all_sales_channels": false,
|
||||
"limit_sales_channels": ["web"],
|
||||
"sales_channels": ["web"],
|
||||
"available_from": null,
|
||||
"available_from_mode": "hide",
|
||||
|
||||
@@ -42,6 +42,8 @@ payment_date date **DEPRECATED AN
|
||||
payment_provider string **DEPRECATED AND INACCURATE** Payment provider used for this order
|
||||
total money (string) Total value of this order
|
||||
comment string Internal comment on this order
|
||||
api_meta object Meta data for that order. Only available through API, no guarantees
|
||||
on the content structure. You can use this to save references to your system.
|
||||
custom_followup_at date Internal date for a custom follow-up action
|
||||
checkin_attention boolean If ``true``, the check-in app should show a warning
|
||||
that this ticket requires special attention if a ticket
|
||||
@@ -215,6 +217,11 @@ answers list of objects Answers to user
|
||||
seat objects The assigned seat. Can be ``null``.
|
||||
├ id integer Internal ID of the seat instance
|
||||
├ name string Human-readable seat name
|
||||
├ zone_name string Name of the zone the seat is in
|
||||
├ row_name string Name/number of the row the seat is in
|
||||
├ row_label string Additional label of the row (or ``null``)
|
||||
├ seat_number string Number of the seat within the row
|
||||
├ seat_label string Additional label of the seat (or ``null``)
|
||||
└ seat_guid string Identifier of the seat within the seating plan
|
||||
pdf_data object Data object required for ticket PDF generation. By default,
|
||||
this field is missing. It will be added only if you add the
|
||||
@@ -455,10 +462,13 @@ List of all orders
|
||||
:query datetime modified_since: Only return orders that have changed since the given date. Be careful: We only
|
||||
recommend using this in combination with ``testmode=false``, since test mode orders can vanish at any time and
|
||||
you will not notice it using this method.
|
||||
:query datetime created_since: Only return orders that have been created since the given date.
|
||||
:query datetime created_since: Only return orders that have been created since the given date (inclusive).
|
||||
:query datetime created_before: Only return orders that have been created before the given date (exclusive).
|
||||
:query integer subevent: Only return orders with a position that contains this subevent ID. *Warning:* Result will also include orders if they contain mixed subevents, and it will even return orders where the subevent is only contained in a canceled position.
|
||||
:query datetime subevent_after: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive after, and it considers the **end** of the subevent (or its start, if the end is not set).
|
||||
:query datetime subevent_before: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive before, and it considers the **start** of the subevent.
|
||||
:query string sales_channel: Only return orders with the given sales channel identifier (e.g. ``"web"``).
|
||||
:query string payment_provider: Only return orders that contain a payment using the given payment provider. Note that this also searches for partial incomplete, or failed payments within the order and is not useful to get a sum of payment amounts without further processing.
|
||||
:query string exclude: Exclude a field from the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times.
|
||||
:query string include: Include only the given field in the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times. ``include`` is applied before ``exclude``, so ``exclude`` takes precedence.
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -554,6 +564,7 @@ Fetching individual orders
|
||||
"fees": [],
|
||||
"total": "23.00",
|
||||
"comment": "",
|
||||
"api_meta": {},
|
||||
"custom_followup_at": null,
|
||||
"checkin_attention": false,
|
||||
"checkin_text": null,
|
||||
@@ -734,6 +745,8 @@ Updating order fields
|
||||
|
||||
* ``comment``
|
||||
|
||||
* ``api_meta``
|
||||
|
||||
* ``custom_followup_at``
|
||||
|
||||
* ``invoice_address`` (you always need to supply the full object, or ``null`` to delete the current address)
|
||||
|
||||
219
doc/api/resources/saleschannels.rst
Normal file
219
doc/api/resources/saleschannels.rst
Normal file
@@ -0,0 +1,219 @@
|
||||
Sales channels
|
||||
==============
|
||||
|
||||
Resource description
|
||||
--------------------
|
||||
|
||||
The sales channel resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
identifier string Internal ID of the sales channel. For sales channel types
|
||||
that allow only one instance, this is the same as ``type``.
|
||||
For sales channel types that allow multiple instances, this
|
||||
is always prefixed with ``type.``.
|
||||
label multi-lingual string Human-readable name of the sales channel
|
||||
type string Type of the sales channel. Only channels with type ``api``
|
||||
can currently be created through the API.
|
||||
position integer Position for sorting lists of sales channels
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/saleschannels/
|
||||
|
||||
Returns a list of all sales channels within a given organizer.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/saleschannels/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"identifier": "web",
|
||||
"name": {
|
||||
"en": "Online shop"
|
||||
},
|
||||
"type": "web",
|
||||
"position": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query integer page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/saleschannels/(identifier)/
|
||||
|
||||
Returns information on one sales channel, identified by its identifier.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/saleschannels/web/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"identifier": "web",
|
||||
"name": {
|
||||
"en": "Online shop"
|
||||
},
|
||||
"type": "web",
|
||||
"position": 0
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param identifier: The ``identifier`` field of the sales channel to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:post:: /api/v1/organizers/(organizer)/saleschannels/
|
||||
|
||||
Creates a sales channel
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/organizers/bigevents/saleschannels/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"identifier": "api.custom",
|
||||
"name": {
|
||||
"en": "Custom integration"
|
||||
},
|
||||
"type": "api",
|
||||
"position": 2
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"identifier": "api.custom",
|
||||
"name": {
|
||||
"en": "Custom integration"
|
||||
},
|
||||
"type": "api",
|
||||
"position": 2
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create a sales channel for
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The sales channel could not be created due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/saleschannels/(identifier)/
|
||||
|
||||
Update a sales channel. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of
|
||||
the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you
|
||||
want to change.
|
||||
|
||||
You can change all fields of the resource except the ``identifier`` and ``type`` fields.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/saleschannels/web/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"position": 5
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"identifier": "web",
|
||||
"name": {
|
||||
"en": "Online shop"
|
||||
},
|
||||
"type": "web",
|
||||
"position": 5
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param identifier: The ``identifier`` field of the sales channel to modify
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The sales channel could not be modified due to invalid submitted data
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
|
||||
|
||||
.. http:delete:: /api/v1/organizers/(organizer)/saleschannels/(identifier)/
|
||||
|
||||
Delete a sales channel. You can not delete sales channels which have already been used or which are integral parts
|
||||
of the system.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /api/v1/organizers/bigevents/saleschannels/api.custom/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
Vary: Accept
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
:param identifier: The ``identifier`` field of the sales channel to delete
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource **or** the sales channel is currently in use.
|
||||
@@ -22,6 +22,8 @@ id integer Internal ID of
|
||||
name string Team name
|
||||
all_events boolean Whether this team has access to all events
|
||||
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
|
||||
@@ -122,6 +124,7 @@ Team endpoints
|
||||
"name": "Admin team",
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
"require_2fa": true,
|
||||
"can_create_events": true,
|
||||
...
|
||||
}
|
||||
@@ -159,6 +162,7 @@ Team endpoints
|
||||
"name": "Admin team",
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
"require_2fa": true,
|
||||
"can_create_events": true,
|
||||
...
|
||||
}
|
||||
@@ -186,6 +190,7 @@ Team endpoints
|
||||
"name": "Admin team",
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
"require_2fa": true,
|
||||
"can_create_events": true,
|
||||
...
|
||||
}
|
||||
@@ -203,6 +208,7 @@ Team endpoints
|
||||
"name": "Admin team",
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
"require_2fa": true,
|
||||
"can_create_events": true,
|
||||
...
|
||||
}
|
||||
@@ -246,6 +252,7 @@ Team endpoints
|
||||
"name": "Admin team",
|
||||
"all_events": true,
|
||||
"limit_events": [],
|
||||
"require_2fa": true,
|
||||
"can_create_events": true,
|
||||
...
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ The following values for ``action_types`` are valid with pretix core:
|
||||
* ``pretix.event.order.modified``
|
||||
* ``pretix.event.order.contact.changed``
|
||||
* ``pretix.event.order.changed.*``
|
||||
* ``pretix.event.order.deleted`` (can only occur for test mode orders)
|
||||
* ``pretix.event.order.refund.created``
|
||||
* ``pretix.event.order.refund.created.externally``
|
||||
* ``pretix.event.order.refund.requested``
|
||||
|
||||
@@ -12,7 +12,7 @@ Core
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
|
||||
item_copy_data, register_sales_channels, register_global_settings, quota_availability, global_email_filter,
|
||||
item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter,
|
||||
register_ticket_secret_generators, gift_card_transaction_display,
|
||||
register_text_placeholders, register_mail_placeholders
|
||||
|
||||
@@ -35,11 +35,11 @@ Frontend
|
||||
--------
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header
|
||||
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header, seatingframe_html_head
|
||||
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: order_info, order_info_top, order_meta_from_request
|
||||
:members: order_info, order_info_top, order_meta_from_request, order_api_meta_from_request
|
||||
|
||||
Request flow
|
||||
""""""""""""
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
|
||||
.. _`importcol`:
|
||||
|
||||
Extending the order import process
|
||||
==================================
|
||||
Extending the import process
|
||||
============================
|
||||
|
||||
It's possible through the backend to import orders into pretix, for example from a legacy ticketing system. If your
|
||||
plugins defines additional data structures around orders, it might be useful to make it possible to import them as well.
|
||||
It's possible through the backend to import objects into pretix, for example orders from a legacy ticketing system. If
|
||||
your plugin defines additional data structures around those objects, it might be useful to make it possible to import
|
||||
them as well.
|
||||
|
||||
Import process
|
||||
--------------
|
||||
@@ -40,7 +41,7 @@ Column registration
|
||||
|
||||
The import API does not make a lot of usage from signals, however, it
|
||||
does use a signal to get a list of all available import columns. Your plugin
|
||||
should listen for this signal and return the subclass of ``pretix.base.orderimport.ImportColumn``
|
||||
should listen for this signal and return the subclass of ``pretix.base.modelimport.ImportColumn``
|
||||
that we'll provide in this plugin:
|
||||
|
||||
.. sourcecode:: python
|
||||
@@ -56,10 +57,16 @@ that we'll provide in this plugin:
|
||||
EmailColumn(sender),
|
||||
]
|
||||
|
||||
Similar signals exist for other objects:
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: voucher_import_columns
|
||||
|
||||
|
||||
The column class API
|
||||
--------------------
|
||||
|
||||
.. class:: pretix.base.orderimport.ImportColumn
|
||||
.. class:: pretix.base.modelimport.ImportColumn
|
||||
|
||||
The central object of each import extension is the subclass of ``ImportColumn``.
|
||||
|
||||
|
||||
@@ -84,6 +84,8 @@ convenient to you:
|
||||
|
||||
.. automethod:: _register_fonts
|
||||
|
||||
.. automethod:: _register_event_fonts
|
||||
|
||||
.. automethod:: _on_first_page
|
||||
|
||||
.. automethod:: _on_other_page
|
||||
|
||||
@@ -17,7 +17,7 @@ The project pretix is split into several components. The main components are:
|
||||
create and manage their events, items, orders and tickets.
|
||||
|
||||
**presale**
|
||||
This is the ticket-shop itself, containing all of the parts visible to the
|
||||
This is the ticket shop itself, containing all of the parts visible to the
|
||||
end user. Also called "frontend" in parts of this documentation.
|
||||
|
||||
**api**
|
||||
|
||||
@@ -19,3 +19,4 @@ Contents:
|
||||
permissions
|
||||
logging
|
||||
locking
|
||||
timemachine
|
||||
|
||||
@@ -15,7 +15,7 @@ includes serializers for serializing the following types:
|
||||
* Built-in types: ``int``, ``float``, ``decimal.Decimal``, ``dict``, ``list``, ``bool``
|
||||
* ``datetime.date``, ``datetime.datetime``, ``datetime.time``
|
||||
* ``LazyI18nString``
|
||||
* References to Django ``File`` objects that are already stored in a storage backend
|
||||
* References to Django ``File`` objects that are already stored in a storage backend [#f1]_
|
||||
* References to model instances
|
||||
|
||||
In code, we recommend to always use the ``.get()`` method on the settings object to access a value, but for
|
||||
@@ -55,6 +55,9 @@ You can simply use it like this:
|
||||
"preserve his reservation."),
|
||||
)
|
||||
|
||||
|
||||
.. _settings-defaults-in-plugins:
|
||||
|
||||
Defaults in plugins
|
||||
-------------------
|
||||
|
||||
@@ -70,3 +73,9 @@ Make sure that you include this code in a module that is imported at app loading
|
||||
|
||||
.. _django-hierarkey: https://github.com/raphaelm/django-hierarkey
|
||||
.. _documentation: https://django-hierarkey.readthedocs.io/en/latest/
|
||||
|
||||
.. rubric:: Footnotes
|
||||
|
||||
.. [#f1] If you store ``File`` instances in per-event settings, make sure to always register them with ``add_default``
|
||||
as described above in :ref:`settings-defaults-in-plugins`. Otherwise, the file won't get copied properly if the
|
||||
user copies the settings of an existing event to a new one.
|
||||
|
||||
32
doc/development/implementation/timemachine.rst
Normal file
32
doc/development/implementation/timemachine.rst
Normal file
@@ -0,0 +1,32 @@
|
||||
Time machine mode
|
||||
=================
|
||||
|
||||
In test mode, pretix provides a "time machine" feature which allows event organizers
|
||||
to test their shop as if it were a different date and time. To enable this feature, they can
|
||||
click on the "time machine"-link in the test mode warning box on the event page.
|
||||
|
||||
Internally, this time machine mode is implemented by calling our custom :py:meth:`time_machine_now()`
|
||||
function instead of :py:meth:`django.utils.timezone.now()` in all places where the fake time should be
|
||||
taken into account. If you add code that uses the current date and time for checking whether some
|
||||
product can be bought, you should use :py:meth:`time_machine_now`.
|
||||
|
||||
.. autofunction:: pretix.base.timemachine.time_machine_now
|
||||
|
||||
Background tasks
|
||||
----------------
|
||||
|
||||
The time machine datetime is passed through the request flow via a thread-local variable (ContextVar).
|
||||
Therefore, if you call a background task in the order process, where time_machine_now should be
|
||||
respected, you need to pass it through manually as shown in the example below:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@app.task()
|
||||
def my_task(self, override_now_dt: datetime=None) -> None:
|
||||
with time_machine_now_assigned(override_now_dt):
|
||||
# ...do something that uses time_machine_now()
|
||||
|
||||
my_task.apply_async(kwargs={'override_now_dt': time_machine_now(default=None)})
|
||||
|
||||
|
||||
.. autofunction:: pretix.base.timemachine.time_machine_now_assigned
|
||||
@@ -90,6 +90,10 @@ as its first argument and can be used like this::
|
||||
<a href="{% eventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
|
||||
<a href="{% abseventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
|
||||
|
||||
To generate absolute URLs on the main domain, you can use the ``absurl`` template tag::
|
||||
|
||||
{% load eventurl %}
|
||||
<a href="{% absmainurl "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}">Event settings</a>
|
||||
|
||||
Implementation details
|
||||
----------------------
|
||||
|
||||
@@ -211,5 +211,15 @@ with the documentation a lot, you might find it useful to use sphinx-autobuild::
|
||||
Then, go to http://localhost:8081 for a version of the documentation that automatically re-builds
|
||||
whenever you change a source file.
|
||||
|
||||
Working with frontend assets
|
||||
----------------------------
|
||||
|
||||
To update the frontend styles of shops with a custom styling, run the following commands inside
|
||||
your virtual environment.::
|
||||
|
||||
python -m pretix collectstatic --noinput
|
||||
python -m pretix updateassets
|
||||
|
||||
|
||||
.. _Django's documentation: https://docs.djangoproject.com/en/1.11/ref/django-admin/#runserver
|
||||
.. _pretixdroid: https://github.com/pretix/pretixdroid
|
||||
|
||||
@@ -31,7 +31,7 @@ pretix/
|
||||
Additional code implementing our customized :ref:`URL handling <urlconf>`.
|
||||
|
||||
static/
|
||||
Contains all static files (CSS/SASS, JavaScript, images) of pretix' core
|
||||
Contains all static files (CSS/SASS, JavaScript, images) of pretix' core.
|
||||
We use libsass as a preprocessor for CSS. Our own sass code is built in the same
|
||||
step as Bootstrap and FontAwesome, so their mixins etc. are fully available.
|
||||
|
||||
@@ -41,6 +41,6 @@ pretix/
|
||||
|
||||
tests/
|
||||
This is the root directory for all test codes. It includes subdirectories ``api``, ``base``,
|
||||
``control``, ``presale``, ``helpers`, ``multidomain`` and ``plugins`` to mirror the structure
|
||||
``control``, ``presale``, ``helpers``, ``multidomain`` and ``plugins`` to mirror the structure
|
||||
of the pretix source code as well as ``testdummy``, which is a pretix plugin used during
|
||||
testing.
|
||||
|
||||
@@ -34,6 +34,7 @@ internal_id string Can be used for
|
||||
contact_name string Contact person (or ``null``)
|
||||
contact_name_parts object of strings Decomposition of contact name (i.e. given name, family name)
|
||||
contact_email string Contact person email address (or ``null``)
|
||||
contact_cc_email string Copy email addresses, can be multiple separated by comma (or ``null``)
|
||||
booth string Booth number (or ``null``). Maximum 100 characters.
|
||||
locale string Locale for communication with the exhibitor.
|
||||
access_code string Access code for the exhibitor to access their data or use the lead scanning app (read-only).
|
||||
@@ -44,6 +45,8 @@ allow_voucher_access boolean Enables access
|
||||
lead_scanning_scope_by_device string Enables lead scanning to be handled as one lead per attendee
|
||||
per scanning device, instead of only per exhibitor.
|
||||
comment string Internal comment, not shown to exhibitor
|
||||
exhibitor_tags list of strings Internal tags to categorize exhibitors, not shown to exhibitor.
|
||||
The tags need to be created through the web interface currently.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
You can also access the scanned leads through the API which contains the following public fields:
|
||||
@@ -109,6 +112,7 @@ Endpoints
|
||||
"title": "Dr"
|
||||
},
|
||||
"contact_email": "johnson@as.example.org",
|
||||
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
|
||||
"booth": "A2",
|
||||
"locale": "de",
|
||||
"access_code": "VKHZ2FU84",
|
||||
@@ -117,7 +121,8 @@ Endpoints
|
||||
"allow_lead_scanning": true,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
"comment": ""
|
||||
"comment": "",
|
||||
"exhibitor_tags": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -162,6 +167,7 @@ Endpoints
|
||||
"title": "Dr"
|
||||
},
|
||||
"contact_email": "johnson@as.example.org",
|
||||
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
|
||||
"booth": "A2",
|
||||
"locale": "de",
|
||||
"access_code": "VKHZ2FU84",
|
||||
@@ -170,7 +176,8 @@ Endpoints
|
||||
"allow_lead_scanning": true,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
"comment": ""
|
||||
"comment": "",
|
||||
"exhibitor_tags": []
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -365,12 +372,16 @@ Endpoints
|
||||
"title": "Dr"
|
||||
},
|
||||
"contact_email": "johnson@as.example.org",
|
||||
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
|
||||
"booth": "A2",
|
||||
"locale": "de",
|
||||
"allow_lead_scanning": true,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
"comment": ""
|
||||
"comment": "",
|
||||
"exhibitor_tags": [
|
||||
"Gold Sponsor"
|
||||
]
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -394,6 +405,7 @@ Endpoints
|
||||
"title": "Dr"
|
||||
},
|
||||
"contact_email": "johnson@as.example.org",
|
||||
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
|
||||
"booth": "A2",
|
||||
"locale": "de",
|
||||
"access_code": "VKHZ2FU84",
|
||||
@@ -402,7 +414,10 @@ Endpoints
|
||||
"allow_lead_scanning": true,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
"comment": ""
|
||||
"comment": "",
|
||||
"exhibitor_tags": [
|
||||
"Gold Sponsor"
|
||||
]
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create new exhibitor for
|
||||
@@ -454,6 +469,7 @@ Endpoints
|
||||
"title": "Dr"
|
||||
},
|
||||
"contact_email": "johnson@as.example.org",
|
||||
"contact_cc_email": "miller@as.example.org,smith@as.example.org",
|
||||
"booth": "A2",
|
||||
"locale": "de",
|
||||
"access_code": "VKHZ2FU84",
|
||||
@@ -462,7 +478,10 @@ Endpoints
|
||||
"allow_lead_scanning": true,
|
||||
"allow_lead_access": true,
|
||||
"allow_voucher_access": true,
|
||||
"comment": ""
|
||||
"comment": "",
|
||||
"exhibitor_tags": [
|
||||
"Gold Sponsor"
|
||||
]
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to modify
|
||||
|
||||
105
doc/plugins/getyourguide.rst
Normal file
105
doc/plugins/getyourguide.rst
Normal file
@@ -0,0 +1,105 @@
|
||||
GetYourGuide
|
||||
============
|
||||
|
||||
.. note::
|
||||
|
||||
The GetYourGuide integration is currently in Beta. Please contact support@pretix.eu to enable the integration
|
||||
for your pretix.eu organizer account.
|
||||
|
||||
Introduction
|
||||
------------
|
||||
Using third party aggregators, such als GetYourGuide, event organizers can sell tickets to their events not only on
|
||||
their own ticket-shop but also on the aggregator's portal. While this service is not for free, it allows event
|
||||
organizers to reacher a larger audience that would otherwise not have found their way into the organizers webshop.
|
||||
|
||||
Using pretix' integration with GetYourGuide, event organizers can profit from an additional sales and revenue channel,
|
||||
while keeping the effort for setting up and maintaining multiple ticket shops to a minimum.
|
||||
|
||||
Preparing your organizer account
|
||||
--------------------------------
|
||||
The first step in enabling the GetYourGuide integration, is to setup a corresponding Sales Channel, which will be used
|
||||
to properly attribute the sales generated. This needs to be done only once per organizer account.
|
||||
|
||||
To do so, log into the pretix backend, select ``Organizers`` from the navigation and then the organizer in question.
|
||||
Extending the ``Settings``-menu, find the ``Sales channels`` configuration and click the ``Add a new channel`` button.
|
||||
|
||||
On the following page, you will be able to select ``GetYourGuide`` as the sales channel type and give it a custom name.
|
||||
|
||||
Preparing your event
|
||||
--------------------
|
||||
In order to now sell your events on GetYourGuide, you will need to configure each event in question.
|
||||
|
||||
1. Enabling the plugin
|
||||
Within your event, extend the ``Settings`` menu and navigate to ``Plugins``. Activate the plugin in the
|
||||
``Integrations`` tab.
|
||||
|
||||
2. Sell the event on the sales channel
|
||||
Pick the sales channel or channels, on which you would like to sell your event by navigating to the event's general
|
||||
settings page using the ``Sell on all sales channels`` or ``Restrict to specific sales channels`` checkboxes.
|
||||
|
||||
3. Configure one or more products to be sold on GetYourGuide
|
||||
Either create a new or edit an existing product, that you would like to sell on GetYourGuide. To do so, you will
|
||||
need to have checked the ``Sell on all sales channels`` or appropriate ``Restrict to specific sales channels``
|
||||
checkbox of the product within it's ``Availability`` tab.
|
||||
In addition, you will also need to set the GetYourGuide equivalent ticket category in the product's accordingly
|
||||
named settings tab. Within your event, there can be only one product per ticket category. Depending on your further
|
||||
configuration, you must at least select one product to be in the ``Adult`` or ``Group`` category.
|
||||
|
||||
4. Configuring the GetYourGuide-plugin
|
||||
Once you have configured one or more products to be eligible to be sold on GetYourGuide, you'll need to configure a
|
||||
few basic settings within the event (``Settings`` --> ``GetYourGuide``). The most important settings can be found
|
||||
the in the ``Configuration`` tab, such as the location of the event on sale.
|
||||
|
||||
Ticket Categories
|
||||
-----------------
|
||||
While pretix only uses the ticket category term loosely to group together multiple products for nicer display,
|
||||
GetYourGuide is relying on the ticket categories to price the tickets.
|
||||
|
||||
First of all, you need to make the decision on how you are planning on selling your tickets on GetYourGuide - in most
|
||||
cases, this will reflect your current sales strategy within your pretix shop.
|
||||
|
||||
- Individual tickets
|
||||
Every single person attending will need to purchase their own ticket. A family of two adults and two
|
||||
children will have to purchase and pay for a total of 4 tickets.
|
||||
In this case, you will need to offer *at least* a ticket of the ``Adult`` type, but may offer any other ticket
|
||||
category type (Child, Youth, Senior, ...) in addition. But you cannot offer a ``Group`` ticket.
|
||||
|
||||
- Group tickets
|
||||
Two groups, consisting of 10 and 20 participants respectively, won't need to purchase a total of 30 tickets, but
|
||||
rather two group tickets. It is up to you to configure the group size limits within the GetYourGuide-settings of your
|
||||
product.
|
||||
Choosing this option, you cannot offer any other ticket categories besides ``Group``.
|
||||
|
||||
Setting up event dates and quotas
|
||||
---------------------------------
|
||||
Of course, in addition to creating products, you will also need to add them to a quota for them to be available for
|
||||
sale. The process for doing this is the very same as for any regular event or event series.
|
||||
|
||||
.. note::
|
||||
|
||||
When selling individual tickets through GetYourGuide, you will not be able to offer differing quantities for
|
||||
individual ticket categories.
|
||||
|
||||
For this reason, we recommend to place all GetYourGuide-eligible products into the same quota. Should you however opt
|
||||
to create multiple quotas which create an imbalance, pretix will report only the available number of tickets for the
|
||||
lowest relevant quota.
|
||||
|
||||
Connecting your event to GetYourGuide
|
||||
-------------------------------------
|
||||
Once you have set up your event and products and performed all necessary configuration, you may want to use the
|
||||
Analyzer-feature of our GetYourGuide-plugin (``Settings`` -> ``GetYourGuide`` -> tab ``Analyzer``).
|
||||
|
||||
The Analyzer should not display any blocking error messages and at least one event date that is ready for publishing on
|
||||
the GetYourGuide platform.
|
||||
|
||||
At this point, you will need to setup your event (called ``product`` in the GetYourGuide universe) on their
|
||||
`Supplier Portal`_ and connect it with your pretix shop. To do so, please follow the
|
||||
`Connecting a new product to your Reservation System`_ on the GetYourGuide Supply Partner Help Center.
|
||||
|
||||
Select ``pretix.eu`` as your reservation system; the required ``product ID`` can be found in the ``Configuration`` tab
|
||||
of the GetYourGuide plugin settings page.
|
||||
|
||||
From this point on, GetYourGuide will automatically import the availabilities and products and offer them for sale.
|
||||
|
||||
.. _Supplier Portal: https://suppliers.getyourguide.com/
|
||||
.. _Connecting a new product to your Reservation System: https://supply.getyourguide.support/hc/en-us/articles/18008029689373-Connecting-a-new-product-to-your-Reservation-system
|
||||
@@ -25,3 +25,4 @@ If you want to **create** a plugin, please go to the
|
||||
webinar
|
||||
presale-saml
|
||||
kulturpass
|
||||
getyourguide
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
sphinx==7.2.*
|
||||
sphinx==7.4.*
|
||||
jinja2==3.1.*
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib-httpdomain
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-e ../
|
||||
sphinx==7.2.*
|
||||
sphinx==7.4.*
|
||||
jinja2==3.1.*
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib-httpdomain
|
||||
|
||||
113
doc/user/android-version-support.rst
Normal file
113
doc/user/android-version-support.rst
Normal file
@@ -0,0 +1,113 @@
|
||||
Android version support policy
|
||||
==============================
|
||||
|
||||
Building software for Android always presents a struggle between keeping compatibility with older hardware to save cost
|
||||
and utilizing feature of new Android versions to improve functionality, security and stability. To help you plan ahead,
|
||||
we are publishing our intended schedule. This is to be understood as a minimum commitment, we will only drop support for
|
||||
older versions if there is a technical reason to do so, not because the scheduled time has been reached.
|
||||
|
||||
.. warning:: This is a non-binding document. We will try our very best to not to deprecate support for Android versions
|
||||
earlier than listed here, but for technical or economical reasons, it might become necessary to do so under
|
||||
specific circumstances. Specifically, we might be forced to partially drop support for Android versions
|
||||
earlier where we integrate third-party components into our software. Typical examples would be specific
|
||||
payment terminal or printer types where we use a third-party component provided by the hardware vendor.
|
||||
|
||||
If we no longer support an Android version, it means that we will no longer publish new versions of the app supporting
|
||||
that Android version. This means you are not getting new features or bug fixes, and at some point your app might stop
|
||||
working with the pretix server.
|
||||
|
||||
pretixSCAN
|
||||
----------
|
||||
|
||||
=========================== ==========================================================
|
||||
Android Version Support schedule
|
||||
=========================== ==========================================================
|
||||
Android 14 Support planned until at least 12/2029.
|
||||
Android 13 Support planned until at least 12/2028.
|
||||
Android 12 Support planned until at least 12/2027.
|
||||
Android 11 Support planned until at least 12/2026.
|
||||
Android 10 Support planned until at least 12/2025.
|
||||
Android 9 Support planned until at least 12/2025.
|
||||
Android 8 Support planned until at least 12/2025.
|
||||
Android 7 Support planned until at least 06/2025.
|
||||
Android 6 Support planned until at least 06/2025.
|
||||
Android 5 Support planned until at least 06/2025.
|
||||
Android 4 Support dropped.
|
||||
=========================== ==========================================================
|
||||
|
||||
pretixPOS
|
||||
---------
|
||||
|
||||
=========================== ==========================================================
|
||||
Android Version Support schedule
|
||||
=========================== ==========================================================
|
||||
Android 14 | Support planned until at least 12/2029.
|
||||
| Limited support for Swissbit microSD TSE (only tested devices).
|
||||
Android 13 | Support planned until at least 12/2028.
|
||||
| Limited support for Swissbit microSD TSE (only tested devices).
|
||||
Android 12 | Support planned until at least 12/2027.
|
||||
| Limited support for Swissbit microSD TSE (only tested devices).
|
||||
Android 11 | Support planned until at least 12/2026.
|
||||
| No support for Swissbit microSD TSE.
|
||||
Android 10 Support planned until at least 12/2025.
|
||||
Android 9 Support planned until at least 12/2025.
|
||||
Android 8 | Support planned until at least 12/2025.
|
||||
| Support for Stripe Terminal on some devices to be dropped 05/2024.
|
||||
Android 7 | Support planned until at least 12/2024.
|
||||
| Support for Stripe Terminal to be dropped 05/2024.
|
||||
| No support for Cryptovision TSE.
|
||||
| No support for SumUp.
|
||||
Android 6 | Support planned until at least 12/2024.
|
||||
| No support for Cryptovision TSE.
|
||||
| No support for Fiskal Cloud.
|
||||
| No support for Stripe Terminal.
|
||||
| No support for SumUp.
|
||||
Android 5 | Support planned until at least 12/2024.
|
||||
| No support for Cryptovision TSE.
|
||||
| No support for Fiskal Cloud.
|
||||
| No support for Stripe Terminal.
|
||||
| No support for SumUp.
|
||||
Android 4 Support dropped.
|
||||
=========================== ==========================================================
|
||||
|
||||
pretixPRINT
|
||||
-----------
|
||||
|
||||
=========================== ==========================================================
|
||||
Android Version Support schedule
|
||||
=========================== ==========================================================
|
||||
Android 14 Support planned until at least 12/2029.
|
||||
Android 13 Support planned until at least 12/2028.
|
||||
Android 12 Support planned until at least 12/2027.
|
||||
Android 11 Support planned until at least 12/2026.
|
||||
Android 10 Support planned until at least 12/2025.
|
||||
Android 9 Support planned until at least 12/2025.
|
||||
Android 8 Support planned until at least 12/2025.
|
||||
Android 7 Support planned until at least 06/2025.
|
||||
Android 6 Support planned until at least 06/2025.
|
||||
Android 5 | Support planned until at least 06/2025.
|
||||
| No support for Evolis printers on some devices.
|
||||
Android 4.4 | Support planned until at least 06/2024.
|
||||
| No support for USB printers.
|
||||
| No support for Evolis printers.
|
||||
Android 4 Support dropped.
|
||||
=========================== ==========================================================
|
||||
|
||||
pretixLEAD
|
||||
----------
|
||||
|
||||
=========================== ==========================================================
|
||||
Android Version Support schedule
|
||||
=========================== ==========================================================
|
||||
Android 14 Support planned until at least 12/2029.
|
||||
Android 13 Support planned until at least 12/2028.
|
||||
Android 12 Support planned until at least 12/2027.
|
||||
Android 11 Support planned until at least 12/2026.
|
||||
Android 10 Support planned until at least 12/2025.
|
||||
Android 9 Support planned until at least 12/2025.
|
||||
Android 8 Support planned until at least 12/2025.
|
||||
Android 7 Support planned until at least 12/2024.
|
||||
Android 6 Support planned until at least 12/2024.
|
||||
Android 5 Support planned until at least 12/2024.
|
||||
Android 4 Support dropped.
|
||||
=========================== ==========================================================
|
||||
@@ -17,8 +17,8 @@ and then click "Generate widget code".
|
||||
You will obtain two code snippets that look *roughly* like the following. The first should be embedded into the
|
||||
``<head>`` part of your website, if possible. If this inconvenient, you can put it in the ``<body>`` part as well::
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="https://pretix.eu/demo/democon/widget/v1.css">
|
||||
<script type="text/javascript" src="https://pretix.eu/widget/v1.en.js" async></script>
|
||||
<link rel="stylesheet" type="text/css" href="https://pretix.eu/demo/democon/widget/v1.css" crossorigin>
|
||||
<script type="text/javascript" src="https://pretix.eu/widget/v1.en.js" async crossorigin></script>
|
||||
|
||||
The second snippet should be embedded at the position where the widget should show up::
|
||||
|
||||
@@ -339,9 +339,9 @@ Currently, the following attributes are understood by pretix itself:
|
||||
``data-attendee-name``, which will pre-fill the last part of the name, whatever that is.
|
||||
|
||||
* ``data-invoice-address-FIELD`` will pre-fill the corresponding field of the invoice address. Possible values for
|
||||
``FIELD`` are ``company``, ``street``, ``zipcode``, ``city`` and ``country``, as well as fields specified by the
|
||||
naming scheme such as ``name-title`` or ``name-given-name`` (see above). ``country`` expects a two-character
|
||||
country code.
|
||||
``FIELD`` are ``company``, ``street``, ``zipcode``, ``city``, ``country``, ``internal-reference``, ``vat-id``, and
|
||||
``custom-field``, as well as fields specified by the naming scheme such as ``name-title`` or ``name-given-name``
|
||||
(see above). ``country`` expects a two-character country code.
|
||||
|
||||
* If ``data-fix="true"`` is given, the user will not be able to change the other given values later. This currently
|
||||
only works for the order email address as well as the invoice address. Attendee-level fields and questions can
|
||||
@@ -449,5 +449,24 @@ Further reading:
|
||||
|
||||
* `Stripe Payment Method Domain registration`_
|
||||
|
||||
|
||||
External payment providers and Cross-Origin-Opener-Policy
|
||||
---------------------------------------------------------
|
||||
|
||||
If you use a payment provider that opens a new window during checkout (such as PayPal), be aware that setting
|
||||
``Cross-Origin-Opener-Policy: same-origin`` results in an empty popup-window being opened in the foreground. This is
|
||||
due to JavaScript not having access to the opened window. To mitigate this, you either need to always open the widget’s
|
||||
checkout in a new tab (see :ref:`Always open a new tab`) or set ``Cross-Origin-Opener-Policy: same-origin-allow-popups``
|
||||
|
||||
|
||||
Working with Cross-Origin-Embedder-Policy
|
||||
-----------------------------------------
|
||||
|
||||
The pretix widget is unfortunately not compatible with ``Cross-Origin-Embedder-Policy: require-corp``. If you include
|
||||
the ``crossorigin`` attributes on the ``<script>`` and ``<link>`` tag as shown above, the widget can show a calendar
|
||||
or product list, but will not be able to open the checkout process in an iframe. If you also set
|
||||
``Cross-Origin-Opener-Policy: same-origin``, the widget can auto-detect that it is running in an isolated enviroment
|
||||
and will instead open the checkout process in a new tab.
|
||||
|
||||
.. _Let's Encrypt: https://letsencrypt.org/
|
||||
.. _Stripe Payment Method Domain registration: https://stripe.com/docs/payments/payment-methods/pmd-registration
|
||||
|
||||
@@ -16,4 +16,5 @@ wanting to use pretix to sell tickets.
|
||||
events/giftcards
|
||||
faq
|
||||
markdown
|
||||
android-version-support
|
||||
glossary
|
||||
|
||||
@@ -11,6 +11,9 @@ In many places of your shop, like frontpage texts, product descriptions and emai
|
||||
since it is way easier to learn than languages like HTML but allows all basic formatting options required
|
||||
for text in those places.
|
||||
|
||||
.. note:: Some fields that are used in one-line context only allow formatting that refers to individual words
|
||||
(such as bold or italic font or a link) but do not allow block-level formatting like lists or headlines.
|
||||
|
||||
Formatting rules
|
||||
----------------
|
||||
|
||||
|
||||
@@ -30,42 +30,42 @@ dependencies = [
|
||||
"babel",
|
||||
"BeautifulSoup4==4.12.*",
|
||||
"bleach==5.0.*",
|
||||
"celery==5.3.*",
|
||||
"celery==5.4.*",
|
||||
"chardet==5.2.*",
|
||||
"cryptography>=3.4.2",
|
||||
"css-inline==0.13.*",
|
||||
"css-inline==0.14.*",
|
||||
"defusedcsv>=1.1.0",
|
||||
"dj-static",
|
||||
"Django==4.2.*",
|
||||
"django-bootstrap3==23.6.*",
|
||||
"django-compressor==4.4",
|
||||
"django-countries==7.5.*",
|
||||
"django-filter==23.5",
|
||||
"Django[argon2]==4.2.*",
|
||||
"django-bootstrap3==24.2",
|
||||
"django-compressor==4.5.1",
|
||||
"django-countries==7.6.*",
|
||||
"django-filter==24.2",
|
||||
"django-formset-js-improved==0.5.0.3",
|
||||
"django-formtools==2.5.1",
|
||||
"django-hierarkey==1.1.*",
|
||||
"django-hijack==3.4.*",
|
||||
"django-hierarkey==1.2.*",
|
||||
"django-hijack==3.6.*",
|
||||
"django-i18nfield==1.9.*,>=1.9.4",
|
||||
"django-libsass==0.9",
|
||||
"django-localflavor==4.0",
|
||||
"django-markup",
|
||||
"django-oauth-toolkit==2.3.*",
|
||||
"django-otp==1.3.*",
|
||||
"django-otp==1.5.*",
|
||||
"django-phonenumber-field==7.3.*",
|
||||
"django-redis==5.4.*",
|
||||
"django-scopes==2.0.*",
|
||||
"django-statici18n==2.4.*",
|
||||
"djangorestframework==3.14.*",
|
||||
"dnspython==2.5.*",
|
||||
"django-statici18n==2.5.*",
|
||||
"djangorestframework==3.15.*",
|
||||
"dnspython==2.6.*",
|
||||
"drf_ujson2==1.7.*",
|
||||
"geoip2==4.*",
|
||||
"importlib_metadata==7.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||
"isoweek",
|
||||
"jsonschema",
|
||||
"kombu==5.3.*",
|
||||
"libsass==0.23.*",
|
||||
"lxml",
|
||||
"markdown==3.5.2", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
||||
"markdown==3.6", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
|
||||
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
|
||||
"mt-940==4.30.*",
|
||||
"oauthlib==3.2.*",
|
||||
@@ -75,24 +75,24 @@ dependencies = [
|
||||
"paypal-checkout-serversdk==1.0.*",
|
||||
"PyJWT==2.8.*",
|
||||
"phonenumberslite==8.13.*",
|
||||
"Pillow==10.2.*",
|
||||
"Pillow==10.4.*",
|
||||
"pretix-plugin-build",
|
||||
"protobuf==4.25.*",
|
||||
"protobuf==5.27.*",
|
||||
"psycopg2-binary",
|
||||
"pycountry",
|
||||
"pycparser==2.21",
|
||||
"pycparser==2.22",
|
||||
"pycryptodome==3.20.*",
|
||||
"pypdf==3.9.*",
|
||||
"python-bidi==0.4.*", # Support for Arabic in reportlab
|
||||
"python-dateutil==2.8.*",
|
||||
"pypdf==4.3.*",
|
||||
"python-bidi==0.5.*", # Support for Arabic in reportlab
|
||||
"python-dateutil==2.9.*",
|
||||
"pytz",
|
||||
"pytz-deprecation-shim==0.1.*",
|
||||
"pyuca",
|
||||
"qrcode==7.4.*",
|
||||
"redis==5.0.*",
|
||||
"reportlab==4.1.*",
|
||||
"reportlab==4.2.*",
|
||||
"requests==2.31.*",
|
||||
"sentry-sdk==1.40.*",
|
||||
"sentry-sdk==2.10.*",
|
||||
"sepaxml==2.6.*",
|
||||
"slimit",
|
||||
"static3==0.7.*",
|
||||
@@ -100,9 +100,10 @@ dependencies = [
|
||||
"text-unidecode==1.*",
|
||||
"tlds>=2020041600",
|
||||
"tqdm==4.*",
|
||||
"ua-parser==0.18.*",
|
||||
"vat_moss_forked==2020.3.20.0.11.0",
|
||||
"vobject==0.9.*",
|
||||
"webauthn==2.0.*",
|
||||
"webauthn==2.2.*",
|
||||
"zeep==4.2.*"
|
||||
]
|
||||
|
||||
@@ -112,21 +113,21 @@ dev = [
|
||||
"aiohttp==3.9.*",
|
||||
"coverage",
|
||||
"coveralls",
|
||||
"fakeredis==2.21.*",
|
||||
"flake8==7.0.*",
|
||||
"fakeredis==2.23.*",
|
||||
"flake8==7.1.*",
|
||||
"freezegun",
|
||||
"isort==5.13.*",
|
||||
"pep8-naming==0.13.*",
|
||||
"pep8-naming==0.14.*",
|
||||
"potypo",
|
||||
"pytest-asyncio",
|
||||
"pytest-cache",
|
||||
"pytest-cov",
|
||||
"pytest-django==4.*",
|
||||
"pytest-mock==3.12.*",
|
||||
"pytest-rerunfailures==13.*",
|
||||
"pytest-mock==3.14.*",
|
||||
"pytest-rerunfailures==14.*",
|
||||
"pytest-sugar",
|
||||
"pytest-xdist==3.5.*",
|
||||
"pytest==8.0.*",
|
||||
"pytest-xdist==3.6.*",
|
||||
"pytest==8.3.*",
|
||||
"responses",
|
||||
]
|
||||
|
||||
|
||||
4
src/.watchmanconfig
Normal file
4
src/.watchmanconfig
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"ignore_dirs": ["node_modules", "data", "pretix/static", "pretix/locale", "pretix/static.dist"]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ localecompile:
|
||||
./manage.py compilemessages
|
||||
|
||||
localegen:
|
||||
./manage.py makemessages --keep-pot --ignore "pretix/helpers/*" --ignore "pretix/static/npm_dir/*" $(LNGS)
|
||||
./manage.py makemessages --keep-pot --ignore "pretix/static/npm_dir/*" $(LNGS)
|
||||
./manage.py makemessages --keep-pot -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
|
||||
|
||||
staticfiles: jsi18n
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
# 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/>.
|
||||
#
|
||||
__version__ = "2024.2.0"
|
||||
__version__ = "2024.7.1"
|
||||
|
||||
@@ -79,6 +79,7 @@ ALL_LANGUAGES = [
|
||||
('de', _('German')),
|
||||
('de-informal', _('German (informal)')),
|
||||
('ar', _('Arabic')),
|
||||
('ca', _('Catalan')),
|
||||
('zh-hans', _('Chinese (simplified)')),
|
||||
('zh-hant', _('Chinese (traditional)')),
|
||||
('cs', _('Czech')),
|
||||
@@ -98,6 +99,8 @@ ALL_LANGUAGES = [
|
||||
('pt-br', _('Portuguese (Brazil)')),
|
||||
('ro', _('Romanian')),
|
||||
('ru', _('Russian')),
|
||||
('sk', _('Slovak')),
|
||||
('sv', _('Swedish')),
|
||||
('es', _('Spanish')),
|
||||
('tr', _('Turkish')),
|
||||
('uk', _('Ukrainian')),
|
||||
@@ -111,6 +114,7 @@ LANGUAGES_RTL = {
|
||||
LANGUAGES_INCUBATING = {
|
||||
'fi', 'pt-br', 'gl',
|
||||
}
|
||||
LANGUAGES = ALL_LANGUAGES
|
||||
LOCALE_PATHS = [
|
||||
os.path.join(os.path.dirname(__file__), 'locale'),
|
||||
]
|
||||
@@ -234,7 +238,12 @@ COMPRESS_FILTERS = {
|
||||
)
|
||||
}
|
||||
|
||||
CURRENCIES = list(currencies)
|
||||
CURRENCIES = [
|
||||
c for c in currencies
|
||||
if c.alpha_3 not in {
|
||||
'XAG', 'XAU', 'XBA', 'XBB', 'XBC', 'XBD', 'XDR', 'XPD', 'XPT', 'XSU', 'XTS', 'XUA',
|
||||
}
|
||||
]
|
||||
CURRENCY_PLACES = {
|
||||
# default is 2
|
||||
'BIF': 0,
|
||||
|
||||
@@ -39,7 +39,8 @@ from pretix.base.models import Device, Event, User
|
||||
from pretix.base.models.auth import SuperuserPermissionSet
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.helpers.security import (
|
||||
SessionInvalid, SessionReauthRequired, assert_session_valid,
|
||||
Session2FASetupRequired, SessionInvalid, SessionPasswordChangeRequired,
|
||||
SessionReauthRequired, assert_session_valid,
|
||||
)
|
||||
|
||||
|
||||
@@ -66,6 +67,10 @@ class EventPermission(BasePermission):
|
||||
return False
|
||||
except SessionReauthRequired:
|
||||
return False
|
||||
except Session2FASetupRequired:
|
||||
return False
|
||||
except SessionPasswordChangeRequired:
|
||||
return False
|
||||
|
||||
perm_holder = (request.auth if isinstance(request.auth, (Device, TeamAPIToken))
|
||||
else request.user)
|
||||
@@ -144,6 +149,10 @@ class ProfilePermission(BasePermission):
|
||||
return False
|
||||
except SessionReauthRequired:
|
||||
return False
|
||||
except Session2FASetupRequired:
|
||||
return False
|
||||
except SessionPasswordChangeRequired:
|
||||
return False
|
||||
|
||||
if isinstance(request.auth, OAuthAccessToken):
|
||||
if not (request.auth.allow_scopes(['read']) or request.auth.allow_scopes(['profile'])) and request.method in SAFE_METHODS:
|
||||
@@ -166,5 +175,9 @@ class AnyAuthenticatedClientPermission(BasePermission):
|
||||
return False
|
||||
except SessionReauthRequired:
|
||||
return False
|
||||
except Session2FASetupRequired:
|
||||
return False
|
||||
except SessionPasswordChangeRequired:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
#
|
||||
import json
|
||||
|
||||
from django.db.models import prefetch_related_objects
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
|
||||
class AsymmetricField(serializers.Field):
|
||||
@@ -61,3 +63,59 @@ class CompatibleJSONField(serializers.JSONField):
|
||||
if value:
|
||||
return json.loads(value)
|
||||
return value
|
||||
|
||||
|
||||
class SalesChannelMigrationMixin:
|
||||
"""
|
||||
Translates between the old field "sales_channels" and the new field combo "all_sales_channels"/"limit_sales_channels".
|
||||
"""
|
||||
|
||||
@property
|
||||
def organizer(self):
|
||||
if "organizer" in self.context:
|
||||
return self.context["organizer"]
|
||||
elif "event" in self.context:
|
||||
return self.context["event"].organizer
|
||||
else:
|
||||
raise ValueError("organizer not in context")
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if "sales_channels" in data:
|
||||
prefetch_related_objects([self.organizer], "sales_channels")
|
||||
all_channels = {
|
||||
s.identifier for s in
|
||||
self.organizer.sales_channels.all()
|
||||
}
|
||||
|
||||
if data.get("all_sales_channels") and set(data["sales_channels"]) != all_channels:
|
||||
raise ValidationError(
|
||||
"If 'all_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||
"the list of all sales channels."
|
||||
)
|
||||
|
||||
if data.get("limit_sales_channels") and set(data["sales_channels"]) != set(data["limit_sales_channels"]):
|
||||
raise ValidationError(
|
||||
"If 'limit_sales_channels' is set, the legacy attribute 'sales_channels' must not be set or set to "
|
||||
"the same list."
|
||||
)
|
||||
|
||||
if data["sales_channels"] == all_channels:
|
||||
data["all_sales_channels"] = True
|
||||
data["limit_sales_channels"] = []
|
||||
else:
|
||||
data["all_sales_channels"] = False
|
||||
data["limit_sales_channels"] = data["sales_channels"]
|
||||
del data["sales_channels"]
|
||||
return super().to_internal_value(data)
|
||||
|
||||
def to_representation(self, value):
|
||||
value = super().to_representation(value)
|
||||
if value.get("all_sales_channels"):
|
||||
prefetch_related_objects([self.organizer], "sales_channels")
|
||||
value["sales_channels"] = sorted([
|
||||
s.identifier for s in
|
||||
self.organizer.sales_channels.all()
|
||||
])
|
||||
else:
|
||||
value["sales_channels"] = value["limit_sales_channels"]
|
||||
return value
|
||||
|
||||
@@ -33,7 +33,7 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.order import (
|
||||
AnswerCreateSerializer, AnswerSerializer, InlineSeatSerializer,
|
||||
)
|
||||
from pretix.base.models import Seat, Voucher
|
||||
from pretix.base.models import SalesChannel, Seat, Voucher
|
||||
from pretix.base.models.orders import CartPosition
|
||||
|
||||
|
||||
@@ -212,7 +212,11 @@ class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
|
||||
addons = BaseCartPositionCreateSerializer(many=True, required=False)
|
||||
bundled = BaseCartPositionCreateSerializer(many=True, required=False)
|
||||
seat = serializers.CharField(required=False, allow_null=True)
|
||||
sales_channel = serializers.CharField(required=False, default='sales_channel')
|
||||
sales_channel = serializers.SlugRelatedField(
|
||||
slug_field='identifier',
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
)
|
||||
voucher = serializers.CharField(required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
@@ -221,6 +225,10 @@ class CartPositionCreateSerializer(BaseCartPositionCreateSerializer):
|
||||
'cart_id', 'expires', 'addons', 'bundled', 'seat', 'sales_channel', 'voucher'
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||
|
||||
def validate_cart_id(self, cid):
|
||||
if cid and not cid.endswith('@api'):
|
||||
raise ValidationError('Cart ID should end in @api or be empty.')
|
||||
|
||||
@@ -25,14 +25,20 @@ from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.event import SubEventSerializer
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.media import MEDIA_TYPES
|
||||
from pretix.base.models import Checkin, CheckinList
|
||||
from pretix.base.models import Checkin, CheckinList, SalesChannel
|
||||
|
||||
|
||||
class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
checkin_count = serializers.IntegerField(read_only=True)
|
||||
position_count = serializers.IntegerField(read_only=True)
|
||||
auto_checkin_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CheckinList
|
||||
@@ -43,6 +49,8 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['auto_checkin_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
|
||||
if 'subevent' in self.context['request'].query_params.getlist('expand'):
|
||||
self.fields['subevent'] = SubEventSerializer(read_only=True)
|
||||
|
||||
@@ -72,10 +80,6 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
if full_data.get('subevent'):
|
||||
raise ValidationError(_('The subevent does not belong to this event.'))
|
||||
|
||||
for channel in full_data.get('auto_checkin_sales_channels') or []:
|
||||
if channel not in get_all_sales_channels():
|
||||
raise ValidationError(_('Unknown sales channel.'))
|
||||
|
||||
CheckinList.validate_rules(data.get('rules'))
|
||||
|
||||
return data
|
||||
|
||||
@@ -19,18 +19,27 @@
|
||||
# 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 rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers import SalesChannelMigrationMixin
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import Discount
|
||||
from pretix.base.models import Discount, SalesChannel
|
||||
|
||||
|
||||
class DiscountSerializer(I18nAwareModelSerializer):
|
||||
class DiscountSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Discount
|
||||
fields = ('id', 'active', 'internal_name', 'position', 'sales_channels', 'available_from',
|
||||
'available_until', 'subevent_mode', 'condition_all_products', 'condition_limit_products',
|
||||
'condition_apply_to_addons', 'condition_min_count', 'condition_min_value',
|
||||
fields = ('id', 'active', 'internal_name', 'position', 'all_sales_channels', 'limit_sales_channels',
|
||||
'available_from', 'available_until', 'subevent_mode', 'condition_all_products',
|
||||
'condition_limit_products', 'condition_apply_to_addons', 'condition_min_count', 'condition_min_value',
|
||||
'benefit_discount_matching_percent', 'benefit_only_apply_to_cheapest_n_matches',
|
||||
'benefit_same_products', 'benefit_limit_products', 'benefit_apply_to_addons',
|
||||
'benefit_ignore_voucher_discounted', 'condition_ignore_voucher_discounted')
|
||||
@@ -39,6 +48,7 @@ class DiscountSerializer(I18nAwareModelSerializer):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['condition_limit_products'].queryset = self.context['event'].items.all()
|
||||
self.fields['benefit_limit_products'].queryset = self.context['event'].items.all()
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
@@ -46,10 +46,14 @@ from rest_framework import serializers
|
||||
from rest_framework.fields import ChoiceField, Field
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
|
||||
from pretix.api.serializers import CompatibleJSONField
|
||||
from pretix.api.serializers import (
|
||||
CompatibleJSONField, SalesChannelMigrationMixin,
|
||||
)
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.settings import SettingsSerializer
|
||||
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
|
||||
from pretix.base.models import (
|
||||
Device, Event, SalesChannel, TaxRule, TeamAPIToken,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import (
|
||||
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
||||
@@ -161,7 +165,7 @@ class ValidKeysField(Field):
|
||||
}
|
||||
|
||||
|
||||
class EventSerializer(I18nAwareModelSerializer):
|
||||
class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
item_meta_properties = MetaPropertyField(required=False, source='*')
|
||||
plugins = PluginsField(required=False, source='*')
|
||||
@@ -170,6 +174,13 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
valid_keys = ValidKeysField(source='*', read_only=True)
|
||||
best_availability_state = serializers.IntegerField(allow_null=True, read_only=True)
|
||||
public_url = serializers.SerializerMethodField('get_event_url', read_only=True)
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
def get_event_url(self, event):
|
||||
return build_absolute_uri(event, 'presale:event.index')
|
||||
@@ -180,7 +191,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
'date_to', 'date_admission', 'is_public', 'presale_start',
|
||||
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
|
||||
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties', 'valid_keys',
|
||||
'sales_channels', 'best_availability_state', 'public_url')
|
||||
'all_sales_channels', 'limit_sales_channels', 'best_availability_state', 'public_url')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -188,6 +199,7 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
self.fields.pop('valid_keys')
|
||||
if not self.context.get('request') or 'with_availability_for' not in self.context['request'].GET:
|
||||
self.fields.pop('best_availability_state')
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['organizer'].sales_channels.all()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -269,13 +281,17 @@ class EventSerializer(I18nAwareModelSerializer):
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
|
||||
plugins_available = {
|
||||
p.module for p in get_all_plugins(self.instance)
|
||||
p.module: p for p in get_all_plugins(self.instance)
|
||||
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
||||
}
|
||||
settings_holder = self.instance if self.instance and self.instance.pk else self.context['organizer']
|
||||
|
||||
for plugin in value.get('plugins'):
|
||||
if plugin not in plugins_available:
|
||||
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
|
||||
if getattr(plugins_available[plugin], 'restricted', False):
|
||||
if plugin not in settings_holder.settings.allowed_restricted_plugins:
|
||||
raise ValidationError(_('Restricted plugin: \'{name}\'.').format(name=plugin))
|
||||
|
||||
return value
|
||||
|
||||
@@ -472,7 +488,8 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
||||
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
|
||||
'presale_start', 'presale_end', 'location', 'geo_lat', 'geo_lon', 'event', 'is_public',
|
||||
'frontpage_text', 'seating_plan', 'item_price_overrides', 'variation_price_overrides',
|
||||
'meta_data', 'seat_category_mapping', 'last_modified', 'best_availability_state')
|
||||
'meta_data', 'seat_category_mapping', 'last_modified', 'best_availability_state',
|
||||
'comment')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -683,10 +700,12 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'locales',
|
||||
'locale',
|
||||
'region',
|
||||
'last_order_modification_date',
|
||||
'allow_modifications',
|
||||
'allow_modifications_after_checkin',
|
||||
'last_order_modification_date',
|
||||
'show_quota_left',
|
||||
'waiting_list_enabled',
|
||||
'waiting_list_auto_disable',
|
||||
'waiting_list_hours',
|
||||
'waiting_list_auto',
|
||||
'waiting_list_names_asked',
|
||||
@@ -733,6 +752,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
||||
'payment_term_accept_late',
|
||||
'payment_explanation',
|
||||
'payment_pending_hidden',
|
||||
'payment_giftcard__enabled',
|
||||
'mail_days_order_expire_warning',
|
||||
'ticket_download',
|
||||
'ticket_download_date',
|
||||
|
||||
@@ -42,19 +42,27 @@ from django.utils.functional import cached_property, lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers import SalesChannelMigrationMixin
|
||||
from pretix.api.serializers.event import MetaDataField
|
||||
from pretix.api.serializers.fields import UploadedFileField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import (
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
|
||||
ItemVariationMetaValue, Question, QuestionOption, Quota,
|
||||
ItemVariationMetaValue, Question, QuestionOption, Quota, SalesChannel,
|
||||
)
|
||||
|
||||
|
||||
class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
||||
class InlineItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
|
||||
coerce_to_string=True)
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
@@ -63,11 +71,14 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||
'checkin_attention', 'checkin_text',
|
||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
'all_sales_channels', 'limit_sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['require_membership_types'].queryset = lazy(lambda: self.context['event'].organizer.membership_types.all(), QuerySet)
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = (
|
||||
self.context['event'].organizer.sales_channels.all() if 'event' in self.context else SalesChannel.objects.none()
|
||||
)
|
||||
|
||||
def validate_meta_data(self, value):
|
||||
for key in value['meta_data'].keys():
|
||||
@@ -76,10 +87,17 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
||||
return value
|
||||
|
||||
|
||||
class ItemVariationSerializer(I18nAwareModelSerializer):
|
||||
class ItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=13,
|
||||
coerce_to_string=True)
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
@@ -88,21 +106,26 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
|
||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||
'checkin_attention', 'checkin_text',
|
||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
'all_sales_channels', 'limit_sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
require_membership_types = validated_data.pop('require_membership_types', [])
|
||||
limit_sales_channels = validated_data.pop('limit_sales_channels', [])
|
||||
variation = ItemVariation.objects.create(**validated_data)
|
||||
|
||||
if require_membership_types:
|
||||
variation.require_membership_types.add(*require_membership_types)
|
||||
|
||||
if limit_sales_channels:
|
||||
variation.limit_sales_channels.add(*limit_sales_channels)
|
||||
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
for key, value in meta_data.items():
|
||||
@@ -223,7 +246,7 @@ class ItemTaxRateField(serializers.Field):
|
||||
return str(Decimal('0.00'))
|
||||
|
||||
|
||||
class ItemSerializer(I18nAwareModelSerializer):
|
||||
class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
addons = InlineItemAddOnSerializer(many=True, required=False)
|
||||
bundles = InlineItemBundleSerializer(many=True, required=False)
|
||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||
@@ -232,11 +255,18 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
||||
'image/png', 'image/jpeg', 'image/gif'
|
||||
), max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
|
||||
'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
|
||||
fields = ('id', 'category', 'name', 'internal_name', 'active', 'all_sales_channels', 'limit_sales_channels',
|
||||
'description', 'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
|
||||
'personalized', 'position', 'picture',
|
||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||
@@ -259,6 +289,8 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
if not self.read_only:
|
||||
self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all()
|
||||
self.fields['grant_membership_type'].queryset = self.context['event'].organizer.membership_types.all()
|
||||
self.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
self.fields['variations'].child.fields['limit_sales_channels'].child_relation.queryset = self.context['event'].organizer.sales_channels.all()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -335,7 +367,10 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
picture = validated_data.pop('picture', None)
|
||||
require_membership_types = validated_data.pop('require_membership_types', [])
|
||||
limit_sales_channels = validated_data.pop('limit_sales_channels', [])
|
||||
item = Item.objects.create(**validated_data)
|
||||
if limit_sales_channels:
|
||||
item.limit_sales_channels.add(*limit_sales_channels)
|
||||
if picture:
|
||||
item.picture.save(os.path.basename(picture.name), picture)
|
||||
if require_membership_types:
|
||||
@@ -343,10 +378,13 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
|
||||
for variation_data in variations_data:
|
||||
require_membership_types = variation_data.pop('require_membership_types', [])
|
||||
limit_sales_channels = variation_data.pop('limit_sales_channels', [])
|
||||
var_meta_data = variation_data.pop('meta_data', {})
|
||||
v = ItemVariation.objects.create(item=item, **variation_data)
|
||||
if require_membership_types:
|
||||
v.require_membership_types.add(*require_membership_types)
|
||||
if limit_sales_channels:
|
||||
v.limit_sales_channels.add(*limit_sales_channels)
|
||||
|
||||
if var_meta_data is not None:
|
||||
for key, value in var_meta_data.items():
|
||||
|
||||
@@ -46,13 +46,12 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.item import (
|
||||
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
||||
)
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, Checkin, Customer, Invoice, InvoiceAddress, InvoiceLine, Item,
|
||||
ItemVariation, Order, OrderPosition, Question, QuestionAnswer,
|
||||
ReusableMedium, Seat, SubEvent, TaxRule, Voucher,
|
||||
ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule, Voucher,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||
@@ -166,7 +165,7 @@ class InlineSeatSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Seat
|
||||
fields = ('id', 'name', 'seat_guid')
|
||||
fields = ('id', 'name', 'seat_guid', 'zone_name', 'row_name', 'row_label', 'seat_label', 'seat_number')
|
||||
|
||||
|
||||
class AnswerSerializer(I18nAwareModelSerializer):
|
||||
@@ -564,6 +563,8 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
attendee_name = AttendeeNameField(source='*')
|
||||
attendee_name_parts = AttendeeNamePartsField(source='*')
|
||||
order__status = serializers.SlugRelatedField(read_only=True, slug_field='status', source='order')
|
||||
order__valid_if_pending = serializers.SlugRelatedField(read_only=True, slug_field='valid_if_pending', source='order')
|
||||
order__require_approval = serializers.SlugRelatedField(read_only=True, slug_field='require_approval', source='order')
|
||||
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
@@ -571,7 +572,8 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'require_attention',
|
||||
'order__status', 'valid_from', 'valid_until', 'blocked')
|
||||
'order__status', 'order__valid_if_pending', 'order__require_approval', 'valid_from', 'valid_until',
|
||||
'blocked')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -583,7 +585,7 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
self.fields['item'] = ItemSerializer(read_only=True, context=self.context)
|
||||
|
||||
if 'variation' in self.context['expand']:
|
||||
self.fields['variation'] = InlineItemVariationSerializer(read_only=True)
|
||||
self.fields['variation'] = InlineItemVariationSerializer(read_only=True, context=self.context)
|
||||
|
||||
if 'answers.question' in self.context['expand']:
|
||||
self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True)
|
||||
@@ -711,6 +713,11 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
payment_provider = OrderPaymentTypeField(source='*', read_only=True)
|
||||
url = OrderURLField(source='*', read_only=True)
|
||||
customer = serializers.SlugRelatedField(slug_field='identifier', read_only=True)
|
||||
sales_channel = serializers.SlugRelatedField(
|
||||
slug_field='identifier',
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
@@ -719,7 +726,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
||||
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
||||
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
||||
'url', 'customer', 'valid_if_pending'
|
||||
'url', 'customer', 'valid_if_pending', 'api_meta'
|
||||
)
|
||||
read_only_fields = (
|
||||
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
||||
@@ -729,6 +736,10 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if "organizer" in self.context:
|
||||
self.fields["sales_channel"].queryset = self.context["organizer"].sales_channels.all()
|
||||
else:
|
||||
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||
if not self.context['pdf_data']:
|
||||
self.fields['positions'].child.fields.pop('pdf_data', None)
|
||||
|
||||
@@ -775,7 +786,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
|
||||
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
|
||||
update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'checkin_text', 'email', 'locale',
|
||||
'phone', 'valid_if_pending']
|
||||
'phone', 'valid_if_pending', 'api_meta']
|
||||
|
||||
if 'invoice_address' in validated_data:
|
||||
iadata = validated_data.pop('invoice_address')
|
||||
@@ -1030,19 +1041,25 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
require_approval = serializers.BooleanField(default=False, required=False)
|
||||
simulate = serializers.BooleanField(default=False, required=False)
|
||||
customer = serializers.SlugRelatedField(slug_field='identifier', queryset=Customer.objects.none(), required=False)
|
||||
sales_channel = serializers.SlugRelatedField(
|
||||
slug_field='identifier',
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['positions'].child.fields['voucher'].queryset = self.context['event'].vouchers.all()
|
||||
self.fields['customer'].queryset = self.context['event'].organizer.customers.all()
|
||||
self.fields['expires'].required = False
|
||||
self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
|
||||
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
|
||||
'require_approval', 'valid_if_pending', 'expires')
|
||||
'require_approval', 'valid_if_pending', 'expires', 'api_meta')
|
||||
|
||||
def validate_payment_provider(self, pp):
|
||||
if pp is None:
|
||||
@@ -1056,11 +1073,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError('Expiration date must be in the future.')
|
||||
return expires
|
||||
|
||||
def validate_sales_channel(self, channel):
|
||||
if channel not in get_all_sales_channels():
|
||||
raise ValidationError('Unknown sales channel.')
|
||||
return channel
|
||||
|
||||
def validate_code(self, code):
|
||||
if code and Order.objects.filter(event__organizer=self.context['event'].organizer, code=code).exists():
|
||||
raise ValidationError(
|
||||
@@ -1122,20 +1134,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
raise ValidationError(errs)
|
||||
return data
|
||||
|
||||
def validate_testmode(self, testmode):
|
||||
if 'sales_channel' in self.initial_data:
|
||||
try:
|
||||
sales_channel = get_all_sales_channels()[self.initial_data['sales_channel']]
|
||||
|
||||
if testmode and not sales_channel.testmode_supported:
|
||||
raise ValidationError('This sales channel does not provide support for test mode.')
|
||||
except KeyError:
|
||||
# We do not need to raise a ValidationError here, since there is another check to validate the
|
||||
# sales_channel
|
||||
pass
|
||||
|
||||
return testmode
|
||||
|
||||
def create(self, validated_data):
|
||||
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
|
||||
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
|
||||
@@ -1144,9 +1142,16 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
payment_date = validated_data.pop('payment_date', now())
|
||||
force = validated_data.pop('force', False)
|
||||
simulate = validated_data.pop('simulate', False)
|
||||
|
||||
if not validated_data.get("sales_channel"):
|
||||
validated_data["sales_channel"] = self.context['event'].organizer.sales_channels.get(identifier="web")
|
||||
|
||||
if validated_data.get("testmode") and not validated_data["sales_channel"].type_instance.testmode_supported:
|
||||
raise ValidationError({"testmode": ["This sales channel does not provide support for test mode."]})
|
||||
|
||||
self._send_mail = validated_data.pop('send_email', False)
|
||||
if self._send_mail is None:
|
||||
self._send_mail = validated_data.get('sales_channel') in self.context['event'].settings.mail_sales_channel_placed_paid
|
||||
self._send_mail = validated_data["sales_channel"].identifier in self.context['event'].settings.mail_sales_channel_placed_paid
|
||||
|
||||
if 'invoice_address' in validated_data:
|
||||
iadata = validated_data.pop('invoice_address')
|
||||
@@ -1306,7 +1311,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
errs[i]['seat'] = ['The specified seat does not exist.']
|
||||
else:
|
||||
seat_usage[seat] += 1
|
||||
if (seat_usage[seat] > 0 and not seat.is_available(sales_channel=validated_data.get('sales_channel', 'web'))) or seat_usage[seat] > 1:
|
||||
sales_channel_id = validated_data['sales_channel'].identifier
|
||||
if (seat_usage[seat] > 0 and not seat.is_available(sales_channel=sales_channel_id)) or seat_usage[seat] > 1:
|
||||
errs[i]['seat'] = [gettext_lazy('The selected seat "{seat}" is not available.').format(seat=seat.name)]
|
||||
elif seated:
|
||||
errs[i]['seat'] = ['The specified product requires to choose a seat.']
|
||||
@@ -1315,7 +1321,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
if 'valid_from' not in pos_data and 'valid_until' not in pos_data:
|
||||
valid_from, valid_until = pos_data['item'].compute_validity(
|
||||
requested_start=(
|
||||
max(requested_valid_from, now())
|
||||
requested_valid_from
|
||||
if requested_valid_from and pos_data['item'].validity_dynamic_start_choice
|
||||
else now()
|
||||
),
|
||||
@@ -1365,6 +1371,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
|
||||
if validated_data.get('locale', None) is None:
|
||||
validated_data['locale'] = self.context['event'].settings.locale
|
||||
|
||||
order = Order(event=self.context['event'], **validated_data)
|
||||
if not validated_data.get('expires'):
|
||||
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
|
||||
@@ -1439,6 +1446,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
if not pos.item.tax_rule or pos.item.tax_rule.price_includes_tax:
|
||||
price_after_voucher = max(pos.price, pos.voucher.calculate_price(listed_price))
|
||||
else:
|
||||
pos._calculate_tax(invoice_address=ia)
|
||||
price_after_voucher = max(pos.price - pos.tax_value, pos.voucher.calculate_price(listed_price))
|
||||
else:
|
||||
price_after_voucher = listed_price
|
||||
@@ -1466,7 +1474,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
answers_data = pos_data.pop('answers', [])
|
||||
use_reusable_medium = pos_data.pop('use_reusable_medium', None)
|
||||
pos = pos_data['__instance']
|
||||
pos._calculate_tax()
|
||||
pos._calculate_tax(invoice_address=ia)
|
||||
|
||||
if simulate:
|
||||
pos = WrappedModel(pos)
|
||||
@@ -1585,7 +1593,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider:
|
||||
payment_provider = 'free'
|
||||
|
||||
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
|
||||
if order.total != Decimal('0.00') and order.event.currency == "XXX":
|
||||
raise ValidationError('Paid products not supported without a valid currency.')
|
||||
|
||||
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID and not validated_data.get('require_approval'):
|
||||
order.status = Order.STATUS_PAID
|
||||
order.save()
|
||||
order.payments.create(
|
||||
@@ -1597,6 +1608,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
elif validated_data.get('status') == Order.STATUS_PAID:
|
||||
if not payment_provider:
|
||||
raise ValidationError('You cannot create a paid order without a payment provider.')
|
||||
if validated_data.get('require_approval'):
|
||||
raise ValidationError('You cannot create a paid order that requires approval.')
|
||||
order.payments.create(
|
||||
amount=order.total,
|
||||
provider=payment_provider,
|
||||
|
||||
@@ -38,7 +38,7 @@ from pretix.base.i18n import get_language_without_region
|
||||
from pretix.base.models import (
|
||||
Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction,
|
||||
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
|
||||
SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||
SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||
)
|
||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
@@ -79,8 +79,8 @@ class CustomerSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Customer
|
||||
fields = ('identifier', 'external_identifier', 'email', 'name', 'name_parts', 'is_active', 'is_verified', 'last_login', 'date_joined',
|
||||
'locale', 'last_modified', 'notes')
|
||||
fields = ('identifier', 'external_identifier', 'email', 'phone', 'name', 'name_parts', 'is_active',
|
||||
'is_verified', 'last_login', 'date_joined', 'locale', 'last_modified', 'notes')
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if instance and instance.provider_id:
|
||||
@@ -165,6 +165,36 @@ class FlexibleTicketRelatedField(serializers.PrimaryKeyRelatedField):
|
||||
self.fail('incorrect_type', data_type=type(data).__name__)
|
||||
|
||||
|
||||
class SalesChannelSerializer(I18nAwareModelSerializer):
|
||||
type = serializers.CharField(default="api")
|
||||
|
||||
class Meta:
|
||||
model = SalesChannel
|
||||
fields = ('identifier', 'type', 'label', 'position')
|
||||
|
||||
def validate_type(self, value):
|
||||
if (not self.instance or not self.instance.pk) and value != "api":
|
||||
raise ValidationError(
|
||||
"You can currently only create channels of type 'api' through the API."
|
||||
)
|
||||
if value and self.instance and self.instance.pk and self.instance.type != value:
|
||||
raise ValidationError(
|
||||
"You cannot change the type of a sales channel."
|
||||
)
|
||||
return value
|
||||
|
||||
def validate_identifier(self, value):
|
||||
if (not self.instance or not self.instance.pk) and not value.startswith("api."):
|
||||
raise ValidationError(
|
||||
"Your identifier needs to start with 'api.'."
|
||||
)
|
||||
if value and self.instance and self.instance.pk and self.instance.identifier != value:
|
||||
raise ValidationError(
|
||||
"You cannot change the identifier of a sales channel."
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class GiftCardSerializer(I18nAwareModelSerializer):
|
||||
value = serializers.DecimalField(max_digits=13, decimal_places=2, min_value=Decimal('0.00'))
|
||||
owner_ticket = FlexibleTicketRelatedField(required=False, allow_null=True, queryset=OrderPosition.all.none())
|
||||
@@ -239,7 +269,7 @@ class TeamSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = (
|
||||
'id', 'name', 'all_events', 'limit_events', 'can_create_events', 'can_change_teams',
|
||||
'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'
|
||||
|
||||
@@ -89,6 +89,7 @@ class SettingsSerializer(serializers.Serializer):
|
||||
except OSError: # pragma: no cover
|
||||
logger.error('Deleting file %s failed.' % fname.name)
|
||||
instance.delete(attr)
|
||||
self.changed_data.append(attr)
|
||||
else:
|
||||
# file is unchanged
|
||||
continue
|
||||
|
||||
@@ -56,6 +56,7 @@ orga_router.register(r'webhooks', webhooks.WebHookViewSet)
|
||||
orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet)
|
||||
orga_router.register(r'giftcards', organizer.GiftCardViewSet)
|
||||
orga_router.register(r'customers', organizer.CustomerViewSet)
|
||||
orga_router.register(r'saleschannels', organizer.SalesChannelViewSet)
|
||||
orga_router.register(r'memberships', organizer.MembershipViewSet)
|
||||
orga_router.register(r'membershiptypes', organizer.MembershipTypeViewSet)
|
||||
orga_router.register(r'reusablemedia', media.ReusableMediaViewSet)
|
||||
|
||||
@@ -211,8 +211,12 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
|
||||
|
||||
if validated_data.get('seat'):
|
||||
# Assumption: Add-ons currently can't have seats, thus we only need to check the main product
|
||||
if validated_data.get('sales_channel'):
|
||||
sales_channel_id = validated_data.get('sales_channel').identifier
|
||||
else:
|
||||
sales_channel_id = "web"
|
||||
if not validated_data['seat'].is_available(
|
||||
sales_channel=validated_data.get('sales_channel', 'web'),
|
||||
sales_channel=sales_channel_id,
|
||||
distance_ignore_cart_id=validated_data['cart_id'],
|
||||
ignore_voucher_id=validated_data['voucher'].pk if validated_data.get('voucher') else None,
|
||||
):
|
||||
|
||||
@@ -115,7 +115,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
if 'subevent' in self.request.query_params.getlist('expand'):
|
||||
qs = qs.prefetch_related(
|
||||
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
|
||||
'subevent__seat_category_mappings', 'subevent__meta_values'
|
||||
'subevent__seat_category_mappings', 'subevent__meta_values', 'auto_checkin_sales_channels'
|
||||
)
|
||||
return qs
|
||||
|
||||
@@ -406,7 +406,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
||||
'item__variations').select_related('item__tax_rule')
|
||||
|
||||
if expand and 'variation' in expand:
|
||||
qs = qs.prefetch_related('variation')
|
||||
qs = qs.prefetch_related('variation', 'variation__meta_values')
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
@@ -60,7 +60,9 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.discounts.all()
|
||||
return self.request.event.discounts.prefetch_related(
|
||||
'limit_sales_channels',
|
||||
)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
|
||||
@@ -41,6 +41,7 @@ from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import serializers, views, viewsets
|
||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.response import Response
|
||||
|
||||
from pretix.api.auth.permission import EventCRUDPermission
|
||||
@@ -57,10 +58,8 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.base.settings import SETTINGS_AFFECTING_CSS
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
from pretix.helpers.i18n import i18ncomp
|
||||
from pretix.presale.style import regenerate_css
|
||||
from pretix.presale.views.organizer import filter_qs_by_attr
|
||||
|
||||
with scopes_disabled():
|
||||
@@ -115,7 +114,10 @@ with scopes_disabled():
|
||||
return queryset.exclude(expr)
|
||||
|
||||
def sales_channel_qs(self, queryset, name, value):
|
||||
return queryset.filter(sales_channels__contains=value)
|
||||
return queryset.filter(
|
||||
Q(all_sales_channels=True) |
|
||||
Q(limit_sales_channels__identifier=value)
|
||||
)
|
||||
|
||||
def search_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
@@ -137,6 +139,12 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
ordering_fields = ('date_from', 'slug')
|
||||
filterset_class = EventFilter
|
||||
|
||||
def get_serializer_context(self):
|
||||
return {
|
||||
**super().get_serializer_context(),
|
||||
"organizer": self.request.organizer,
|
||||
}
|
||||
|
||||
def get_copy_from_queryset(self):
|
||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||
return self.request.auth.get_events_with_any_permission()
|
||||
@@ -155,13 +163,20 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
qs = filter_qs_by_attr(qs, self.request)
|
||||
|
||||
if 'with_availability_for' in self.request.GET:
|
||||
qs = Event.annotated(qs, channel=self.request.GET.get('with_availability_for'))
|
||||
qs = Event.annotated(
|
||||
qs,
|
||||
channel=get_object_or_404(
|
||||
self.request.organizer.sales_channels,
|
||||
identifier=self.request.GET.get('with_availability_for')
|
||||
)
|
||||
)
|
||||
|
||||
return qs.prefetch_related(
|
||||
'organizer',
|
||||
'meta_values',
|
||||
'meta_values__property',
|
||||
'item_meta_properties',
|
||||
'limit_sales_channels',
|
||||
Prefetch(
|
||||
'seat_category_mappings',
|
||||
to_attr='_seat_category_mappings',
|
||||
@@ -190,7 +205,10 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
original_data = self.get_serializer(instance=serializer.instance).data
|
||||
|
||||
current_live_value = serializer.instance.live
|
||||
updated_live_value = serializer.validated_data.get('live', None)
|
||||
current_plugins_value = serializer.instance.get_plugins()
|
||||
@@ -198,6 +216,11 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
|
||||
super().perform_update(serializer)
|
||||
|
||||
if serializer.data == original_data:
|
||||
# Performance optimization: If nothing was changed, we do not need to save or log anything.
|
||||
# This costs us a few cycles on save, but avoids thousands of lines in our log.
|
||||
return
|
||||
|
||||
if updated_live_value is not None and updated_live_value != current_live_value:
|
||||
log_action = 'pretix.event.live.activated' if updated_live_value else 'pretix.event.live.deactivated'
|
||||
serializer.instance.log_action(
|
||||
@@ -262,8 +285,6 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
new_event.is_public = serializer.validated_data['is_public']
|
||||
if 'testmode' in serializer.validated_data:
|
||||
new_event.testmode = serializer.validated_data['testmode']
|
||||
if 'sales_channels' in serializer.validated_data:
|
||||
new_event.sales_channels = serializer.validated_data['sales_channels']
|
||||
if 'has_subevents' in serializer.validated_data:
|
||||
new_event.has_subevents = serializer.validated_data['has_subevents']
|
||||
if 'date_admission' in serializer.validated_data:
|
||||
@@ -271,6 +292,10 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
new_event.save()
|
||||
if 'timezone' in serializer.validated_data:
|
||||
new_event.settings.timezone = serializer.validated_data['timezone']
|
||||
|
||||
if 'all_sales_channels' in serializer.validated_data and 'sales_channels' in serializer.validated_data:
|
||||
new_event.all_sales_channels = serializer.validated_data['all_sales_channels']
|
||||
new_event.limit_sales_channels.set(serializer.validated_data['limit_sales_channels'])
|
||||
else:
|
||||
serializer.instance.set_defaults()
|
||||
|
||||
@@ -373,7 +398,10 @@ with scopes_disabled():
|
||||
return queryset.exclude(expr)
|
||||
|
||||
def sales_channel_qs(self, queryset, name, value):
|
||||
return queryset.filter(event__sales_channels__contains=value)
|
||||
return queryset.filter(
|
||||
Q(event__all_sales_channels=True) |
|
||||
Q(event__limit_sales_channels__identifier=value)
|
||||
)
|
||||
|
||||
def search_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
@@ -415,13 +443,19 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
elif self.request.user.is_authenticated:
|
||||
qs = SubEvent.objects.filter(
|
||||
event__organizer=self.request.organizer,
|
||||
event__in=self.request.user.get_events_with_any_permission()
|
||||
event__in=self.request.user.get_events_with_any_permission(request=self.request)
|
||||
)
|
||||
|
||||
qs = filter_qs_by_attr(qs, self.request)
|
||||
|
||||
if 'with_availability_for' in self.request.GET:
|
||||
qs = SubEvent.annotated(qs, channel=self.request.GET.get('with_availability_for'))
|
||||
qs = SubEvent.annotated(
|
||||
qs,
|
||||
channel=get_object_or_404(
|
||||
self.request.organizer.sales_channels,
|
||||
identifier=self.request.GET.get('with_availability_for')
|
||||
)
|
||||
)
|
||||
|
||||
return qs.prefetch_related(
|
||||
'event',
|
||||
@@ -622,13 +656,12 @@ class EventSettingsView(views.APIView):
|
||||
s.is_valid(raise_exception=True)
|
||||
with transaction.atomic():
|
||||
s.save()
|
||||
self.request.event.log_action(
|
||||
'pretix.event.settings', user=self.request.user, auth=self.request.auth, data={
|
||||
k: v for k, v in s.validated_data.items()
|
||||
}
|
||||
)
|
||||
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
|
||||
regenerate_css.apply_async(args=(request.event.pk,))
|
||||
if s.changed_data:
|
||||
self.request.event.log_action(
|
||||
'pretix.event.settings', user=self.request.user, auth=self.request.auth, data={
|
||||
k: v for k, v in s.validated_data.items()
|
||||
}
|
||||
)
|
||||
s = EventSettingsSerializer(
|
||||
instance=request.event.settings, event=request.event, context={
|
||||
'request': request
|
||||
|
||||
@@ -56,10 +56,17 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
from pretix.helpers.i18n import i18ncomp
|
||||
|
||||
with scopes_disabled():
|
||||
class ItemFilter(FilterSet):
|
||||
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
|
||||
search = django_filters.CharFilter(method='search_qs')
|
||||
|
||||
def search_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
Q(internal_name__icontains=value) | Q(name__icontains=i18ncomp(value))
|
||||
)
|
||||
|
||||
def tax_rate_qs(self, queryset, name, value):
|
||||
if value in ("0", "None", "0.00"):
|
||||
@@ -71,6 +78,18 @@ with scopes_disabled():
|
||||
model = Item
|
||||
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
|
||||
|
||||
class ItemVariationFilter(FilterSet):
|
||||
search = django_filters.CharFilter(method='search_qs')
|
||||
|
||||
def search_qs(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
Q(value__icontains=i18ncomp(value))
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
fields = ['active']
|
||||
|
||||
|
||||
class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = ItemSerializer
|
||||
@@ -87,6 +106,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
'variations', 'addons', 'bundles', 'meta_values', 'meta_values__property',
|
||||
'variations__meta_values', 'variations__meta_values__property',
|
||||
'require_membership_types', 'variations__require_membership_types',
|
||||
'limit_sales_channels', 'variations__limit_sales_channels',
|
||||
).all()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
@@ -139,6 +159,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ItemVariationSerializer
|
||||
queryset = ItemVariation.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter,)
|
||||
filterset_class = ItemVariationFilter
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('id',)
|
||||
permission = None
|
||||
@@ -152,7 +173,8 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
||||
return self.item.variations.all().prefetch_related(
|
||||
'meta_values',
|
||||
'meta_values__property',
|
||||
'require_membership_types'
|
||||
'require_membership_types',
|
||||
'limit_sales_channels',
|
||||
)
|
||||
|
||||
def get_serializer_context(self):
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import datetime
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
from decimal import Decimal
|
||||
@@ -27,7 +28,7 @@ from zoneinfo import ZoneInfo
|
||||
|
||||
import django_filters
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.db.models import (
|
||||
Exists, F, OuterRef, Prefetch, Q, Subquery, prefetch_related_objects,
|
||||
)
|
||||
@@ -96,6 +97,9 @@ from pretix.base.signals import (
|
||||
)
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.control.signals import order_search_filter_q
|
||||
from pretix.helpers import OF_SELF
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
with scopes_disabled():
|
||||
class OrderFilter(FilterSet):
|
||||
@@ -104,6 +108,7 @@ with scopes_disabled():
|
||||
status = django_filters.CharFilter(field_name='status', lookup_expr='iexact')
|
||||
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
|
||||
created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte')
|
||||
created_before = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='lt')
|
||||
subevent_after = django_filters.IsoDateTimeFilter(method='subevent_after_qs')
|
||||
subevent_before = django_filters.IsoDateTimeFilter(method='subevent_before_qs')
|
||||
search = django_filters.CharFilter(method='search_qs')
|
||||
@@ -111,6 +116,8 @@ with scopes_disabled():
|
||||
variation = django_filters.CharFilter(field_name='all_positions', lookup_expr='variation_id', distinct=True)
|
||||
subevent = django_filters.CharFilter(field_name='all_positions', lookup_expr='subevent_id', distinct=True)
|
||||
customer = django_filters.CharFilter(field_name='customer__identifier')
|
||||
sales_channel = django_filters.CharFilter(field_name='sales_channel__identifier')
|
||||
payment_provider = django_filters.CharFilter(method='provider_qs')
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
@@ -134,6 +141,11 @@ with scopes_disabled():
|
||||
)
|
||||
return qs
|
||||
|
||||
def provider_qs(self, qs, name, value):
|
||||
return qs.filter(Exists(
|
||||
OrderPayment.objects.filter(order=OuterRef('pk'), provider=value)
|
||||
))
|
||||
|
||||
def subevent_before_qs(self, qs, name, value):
|
||||
if getattr(self.request, 'event', None):
|
||||
subevents = self.request.event.subevents
|
||||
@@ -225,7 +237,7 @@ class OrderViewSetMixin:
|
||||
if 'customer' not in self.request.GET.getlist('exclude'):
|
||||
qs = qs.select_related('customer')
|
||||
|
||||
qs = qs.prefetch_related(self._positions_prefetch(self.request))
|
||||
qs = qs.select_related('sales_channel').prefetch_related(self._positions_prefetch(self.request))
|
||||
return qs
|
||||
|
||||
def _positions_prefetch(self, request):
|
||||
@@ -312,6 +324,11 @@ class OrganizerOrderViewSet(OrderViewSetMixin, viewsets.ReadOnlyModelViewSet):
|
||||
else:
|
||||
raise PermissionDenied()
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
|
||||
class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
permission = 'can_view_orders'
|
||||
@@ -572,8 +589,10 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
@transaction.atomic()
|
||||
def create_invoice(self, request, **kwargs):
|
||||
order = self.get_object()
|
||||
order = Order.objects.select_for_update(of=OF_SELF).get(pk=order.pk)
|
||||
has_inv = order.invoices.exists() and not (
|
||||
order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
|
||||
and order.invoices.filter(is_cancellation=True).count() >= order.invoices.filter(is_cancellation=False).count()
|
||||
@@ -900,7 +919,11 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
order_modified.send(sender=serializer.instance.event, order=serializer.instance)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save()
|
||||
try:
|
||||
serializer.save()
|
||||
except IntegrityError:
|
||||
logger.exception("Integrity error while saving order")
|
||||
raise ValidationError("Integrity error, possibly duplicate submission of same order.")
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.testmode:
|
||||
@@ -1898,6 +1921,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
return Response(status=204)
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
@transaction.atomic()
|
||||
def reissue(self, request, **kwargs):
|
||||
inv = self.get_object()
|
||||
if inv.canceled:
|
||||
@@ -1905,9 +1929,10 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
elif inv.shredded:
|
||||
raise PermissionDenied('The invoice file is no longer stored on the server.')
|
||||
else:
|
||||
order = Order.objects.select_for_update(of=OF_SELF).get(pk=inv.order_id)
|
||||
c = generate_cancellation(inv)
|
||||
if inv.order.status != Order.STATUS_CANCELED:
|
||||
inv = generate_invoice(inv.order)
|
||||
inv = generate_invoice(order)
|
||||
else:
|
||||
inv = c
|
||||
inv.order.log_action(
|
||||
|
||||
@@ -24,10 +24,11 @@ from decimal import Decimal
|
||||
import django_filters
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.db import transaction
|
||||
from django.db.models import OuterRef, Subquery, Sum
|
||||
from django.db.models import OuterRef, Q, Subquery, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import mixins, serializers, status, views, viewsets
|
||||
@@ -43,18 +44,16 @@ from pretix.api.serializers.organizer import (
|
||||
CustomerCreateSerializer, CustomerSerializer, DeviceSerializer,
|
||||
GiftCardSerializer, GiftCardTransactionSerializer, MembershipSerializer,
|
||||
MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer,
|
||||
SeatingPlanSerializer, TeamAPITokenSerializer, TeamInviteSerializer,
|
||||
TeamMemberSerializer, TeamSerializer,
|
||||
SalesChannelSerializer, SeatingPlanSerializer, TeamAPITokenSerializer,
|
||||
TeamInviteSerializer, TeamMemberSerializer, TeamSerializer,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
Customer, Device, GiftCard, GiftCardTransaction, Membership,
|
||||
MembershipType, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
|
||||
User,
|
||||
MembershipType, Organizer, SalesChannel, SeatingPlan, Team, TeamAPIToken,
|
||||
TeamInvite, User,
|
||||
)
|
||||
from pretix.base.settings import SETTINGS_AFFECTING_CSS
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
from pretix.presale.style import regenerate_organizer_css
|
||||
|
||||
|
||||
class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
@@ -138,11 +137,19 @@ class SeatingPlanViewSet(viewsets.ModelViewSet):
|
||||
with scopes_disabled():
|
||||
class GiftCardFilter(FilterSet):
|
||||
secret = django_filters.CharFilter(field_name='secret', lookup_expr='iexact')
|
||||
expired = django_filters.BooleanFilter(method='expired_qs')
|
||||
value = django_filters.NumberFilter(field_name='cached_value')
|
||||
|
||||
class Meta:
|
||||
model = GiftCard
|
||||
fields = ['secret', 'testmode']
|
||||
|
||||
def expired_qs(self, qs, name, value):
|
||||
if value:
|
||||
return qs.filter(expires__isnull=False, expires__lt=now())
|
||||
else:
|
||||
return qs.filter(Q(expires__isnull=True) | Q(expires__gte=now()))
|
||||
|
||||
|
||||
class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = GiftCardSerializer
|
||||
@@ -504,8 +511,6 @@ class OrganizerSettingsView(views.APIView):
|
||||
k: v for k, v in s.validated_data.items()
|
||||
}
|
||||
)
|
||||
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
|
||||
regenerate_organizer_css.apply_async(args=(request.organizer.pk,))
|
||||
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
|
||||
'request': request
|
||||
})
|
||||
@@ -679,3 +684,68 @@ class MembershipViewSet(viewsets.ModelViewSet):
|
||||
data=self.request.data,
|
||||
)
|
||||
return inst
|
||||
|
||||
|
||||
with scopes_disabled():
|
||||
class SalesChannelFilter(FilterSet):
|
||||
class Meta:
|
||||
model = SalesChannel
|
||||
fields = ['type', 'identifier']
|
||||
|
||||
|
||||
class SalesChannelViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = SalesChannelSerializer
|
||||
queryset = SalesChannel.objects.none()
|
||||
permission = 'can_change_organizer_settings'
|
||||
write_permission = 'can_change_organizer_settings'
|
||||
filter_backends = (DjangoFilterBackend,)
|
||||
filterset_class = SalesChannelFilter
|
||||
lookup_field = 'identifier'
|
||||
lookup_url_kwarg = 'identifier'
|
||||
lookup_value_regex = r"[a-zA-Z0-9.\-_]+"
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.sales_channels.all()
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
inst = serializer.save(
|
||||
organizer=self.request.organizer,
|
||||
type="api"
|
||||
)
|
||||
inst.log_action(
|
||||
'pretix.saleschannel.created',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(self.request.data, {'id': inst.pk})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
inst = serializer.save(
|
||||
type=serializer.instance.type,
|
||||
identifier=serializer.instance.identifier,
|
||||
)
|
||||
inst.log_action(
|
||||
'pretix.sales_channel.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data,
|
||||
)
|
||||
return inst
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
if not instance.allow_delete():
|
||||
raise PermissionDenied("Can only be deleted if unused.")
|
||||
instance.log_action(
|
||||
'pretix.saleschannel.deleted',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'id': instance.pk}
|
||||
)
|
||||
instance.delete()
|
||||
|
||||
@@ -126,6 +126,17 @@ class ParametrizedOrderWebhookEvent(ParametrizedWebhookEvent):
|
||||
}
|
||||
|
||||
|
||||
class DeletedOrderWebhookEvent(ParametrizedWebhookEvent):
|
||||
def build_payload(self, logentry: LogEntry):
|
||||
return {
|
||||
'notification_id': logentry.pk,
|
||||
'organizer': logentry.organizer.slug,
|
||||
'event': logentry.event.slug,
|
||||
'code': logentry.parsed_data.get("code"),
|
||||
'action': logentry.action_type,
|
||||
}
|
||||
|
||||
|
||||
class ParametrizedEventWebhookEvent(ParametrizedWebhookEvent):
|
||||
|
||||
def build_payload(self, logentry: LogEntry):
|
||||
@@ -176,7 +187,7 @@ class ParametrizedItemWebhookEvent(ParametrizedWebhookEvent):
|
||||
}
|
||||
|
||||
|
||||
class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
|
||||
class ParametrizedOrderPositionCheckinWebhookEvent(ParametrizedOrderWebhookEvent):
|
||||
|
||||
def build_payload(self, logentry: LogEntry):
|
||||
d = super().build_payload(logentry)
|
||||
@@ -185,6 +196,7 @@ class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
|
||||
d['orderposition_id'] = logentry.parsed_data.get('position')
|
||||
d['orderposition_positionid'] = logentry.parsed_data.get('positionid')
|
||||
d['checkin_list'] = logentry.parsed_data.get('list')
|
||||
d['type'] = logentry.parsed_data.get('type')
|
||||
d['first_checkin'] = logentry.parsed_data.get('first_checkin')
|
||||
return d
|
||||
|
||||
@@ -296,11 +308,15 @@ def register_default_webhook_events(sender, **kwargs):
|
||||
'pretix.event.order.denied',
|
||||
_('Order denied'),
|
||||
),
|
||||
ParametrizedOrderPositionWebhookEvent(
|
||||
DeletedOrderWebhookEvent(
|
||||
'pretix.event.order.deleted',
|
||||
_('Order deleted'),
|
||||
),
|
||||
ParametrizedOrderPositionCheckinWebhookEvent(
|
||||
'pretix.event.checkin',
|
||||
_('Ticket checked in'),
|
||||
),
|
||||
ParametrizedOrderPositionWebhookEvent(
|
||||
ParametrizedOrderPositionCheckinWebhookEvent(
|
||||
'pretix.event.checkin.reverted',
|
||||
_('Ticket check-in reverted'),
|
||||
),
|
||||
|
||||
@@ -46,7 +46,7 @@ class PretixBaseConfig(AppConfig):
|
||||
from . import invoice # NOQA
|
||||
from . import notifications # NOQA
|
||||
from . import email # NOQA
|
||||
from .services import auth, checkin, currencies, export, mail, tickets, cart, orderimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
|
||||
from .services import auth, checkin, currencies, export, mail, tickets, cart, modelimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
|
||||
from .models import _transactions # NOQA
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
@@ -20,56 +20,83 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.signals import register_sales_channels
|
||||
from pretix.base.signals import (
|
||||
register_sales_channel_types, register_sales_channels,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_ALL_CHANNELS = None
|
||||
_ALL_CHANNEL_TYPES = None
|
||||
|
||||
|
||||
class SalesChannel:
|
||||
class SalesChannelType:
|
||||
def __repr__(self):
|
||||
return '<SalesChannel: {}>'.format(self.identifier)
|
||||
return '<SalesChannelType: {}>'.format(self.identifier)
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
"""
|
||||
The internal identifier of this sales channel.
|
||||
The internal identifier of this sales channel type.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
@property
|
||||
def verbose_name(self) -> str:
|
||||
"""
|
||||
A human-readable name of this sales channel.
|
||||
A human-readable name of this sales channel type.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
"""
|
||||
A human-readable description of this sales channel type.
|
||||
"""
|
||||
return ""
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""
|
||||
The name of a Font Awesome icon to represent this channel
|
||||
This can be:
|
||||
|
||||
- The name of a Font Awesome icon to represent this channel type.
|
||||
- The name of a SVG icon file that is resolvable through the static file system. We recommend to design for a size of 18x14 pixels.
|
||||
"""
|
||||
return "circle"
|
||||
|
||||
@property
|
||||
def default_created(self) -> bool:
|
||||
"""
|
||||
Indication, if a sales channel of this type should automatically be created for every organizer
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def multiple_allowed(self) -> bool:
|
||||
"""
|
||||
Indication, if multiple sales channels of this type may exist in the same organizer
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def testmode_supported(self) -> bool:
|
||||
"""
|
||||
Indication, if a saleschannels supports test mode orders
|
||||
Indication, if a sales channel of this type supports test mode orders
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def payment_restrictions_supported(self) -> bool:
|
||||
"""
|
||||
If this property is ``True``, organizers can restrict the usage of payment providers to this sales channel.
|
||||
If this property is ``True``, organizers can restrict the usage of payment providers to this sales channel type.
|
||||
|
||||
Example: pretixPOS provides its own sales channel, ignores the configured payment providers completely and
|
||||
handles payments locally. Therefor, this property should be set to ``False`` for the pretixPOS sales channel as
|
||||
Example: pretixPOS provides its own sales channel type, ignores the configured payment providers completely and
|
||||
handles payments locally. Therefore, this property should be set to ``False`` for the pretixPOS sales channel as
|
||||
the event organizer cannot restrict the usage of any payment provider through the backend.
|
||||
"""
|
||||
return True
|
||||
@@ -77,8 +104,8 @@ class SalesChannel:
|
||||
@property
|
||||
def unlimited_items_per_order(self) -> bool:
|
||||
"""
|
||||
If this property is ``True``, purchases made using this sales channel are not limited to the maximum amount of
|
||||
items defined in the event settings.
|
||||
If this property is ``True``, purchases made using sales channels of this type are not limited to the maximum
|
||||
amount of items defined in the event settings.
|
||||
"""
|
||||
return False
|
||||
|
||||
@@ -96,34 +123,67 @@ class SalesChannel:
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def required_event_plugin(self) -> str:
|
||||
"""
|
||||
Name of an event plugin that is required for this sales channel to be useful. Defaults to ``None``.
|
||||
"""
|
||||
return
|
||||
|
||||
def get_all_sales_channels():
|
||||
global _ALL_CHANNELS
|
||||
|
||||
if _ALL_CHANNELS:
|
||||
return _ALL_CHANNELS
|
||||
def get_all_sales_channel_types():
|
||||
from pretix.base.signals import register_sales_channel_types
|
||||
global _ALL_CHANNEL_TYPES
|
||||
|
||||
if _ALL_CHANNEL_TYPES:
|
||||
return _ALL_CHANNEL_TYPES
|
||||
|
||||
channels = []
|
||||
for recv, ret in register_sales_channels.send(None):
|
||||
for recv, ret in register_sales_channel_types.send(None):
|
||||
if isinstance(ret, (list, tuple)):
|
||||
channels += ret
|
||||
else:
|
||||
channels.append(ret)
|
||||
for recv, ret in register_sales_channels.send(None): # todo: remove me
|
||||
if isinstance(ret, (list, tuple)):
|
||||
channels += ret
|
||||
else:
|
||||
channels.append(ret)
|
||||
channels.sort(key=lambda c: c.identifier)
|
||||
_ALL_CHANNELS = OrderedDict([(c.identifier, c) for c in channels])
|
||||
if 'web' in _ALL_CHANNELS:
|
||||
_ALL_CHANNELS.move_to_end('web', last=False)
|
||||
return _ALL_CHANNELS
|
||||
_ALL_CHANNEL_TYPES = OrderedDict([(c.identifier, c) for c in channels])
|
||||
if 'web' in _ALL_CHANNEL_TYPES:
|
||||
_ALL_CHANNEL_TYPES.move_to_end('web', last=False)
|
||||
return _ALL_CHANNEL_TYPES
|
||||
|
||||
|
||||
class WebshopSalesChannel(SalesChannel):
|
||||
def get_all_sales_channels():
|
||||
# TODO: remove me
|
||||
warnings.warn('Using get_all_sales_channels() is no longer appropriate, use get_al_sales_channel_types() instead.',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
return get_all_sales_channel_types()
|
||||
|
||||
|
||||
class WebshopSalesChannelType(SalesChannelType):
|
||||
identifier = "web"
|
||||
verbose_name = _('Online shop')
|
||||
icon = "globe"
|
||||
|
||||
|
||||
@receiver(register_sales_channels, dispatch_uid="base_register_default_sales_channels")
|
||||
class ApiSalesChannelType(SalesChannelType):
|
||||
identifier = "api"
|
||||
verbose_name = _('API')
|
||||
description = _('API sales channels come with no built-in functionality, but may be used for custom integrations.')
|
||||
icon = "exchange"
|
||||
default_created = False
|
||||
multiple_allowed = True
|
||||
|
||||
|
||||
SalesChannel = SalesChannelType # TODO: remove me
|
||||
|
||||
|
||||
@receiver(register_sales_channel_types, dispatch_uid="base_register_default_sales_channel_types")
|
||||
def base_sales_channels(sender, **kwargs):
|
||||
return (
|
||||
WebshopSalesChannel(),
|
||||
WebshopSalesChannelType(),
|
||||
ApiSalesChannelType(),
|
||||
)
|
||||
|
||||
@@ -256,7 +256,7 @@ class ListExporter(BaseExporter):
|
||||
ws = wb.create_sheet()
|
||||
self.prepare_xlsx_sheet(ws)
|
||||
try:
|
||||
ws.title = str(self.verbose_name)
|
||||
ws.title = str(self.verbose_name)[:30]
|
||||
except:
|
||||
pass
|
||||
total = 0
|
||||
@@ -374,7 +374,7 @@ class MultiSheetListExporter(ListExporter):
|
||||
wb = SafeWorkbook(write_only=True)
|
||||
n_sheets = len(self.sheets)
|
||||
for i_sheet, (s, l) in enumerate(self.sheets):
|
||||
ws = wb.create_sheet(str(l))
|
||||
ws = wb.create_sheet(str(l)[:30])
|
||||
if hasattr(self, 'prepare_xlsx_sheet_' + s):
|
||||
getattr(self, 'prepare_xlsx_sheet_' + s)(ws)
|
||||
|
||||
|
||||
@@ -39,10 +39,12 @@ from zipfile import ZipFile
|
||||
|
||||
from django import forms
|
||||
from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
|
||||
from pretix.base.models import QuestionAnswer
|
||||
|
||||
from ...control.forms.widgets import Select2
|
||||
from ..exporter import BaseExporter
|
||||
from ..signals import register_data_exporters
|
||||
|
||||
@@ -56,7 +58,7 @@ class AnswerFilesExporter(BaseExporter):
|
||||
|
||||
@property
|
||||
def export_form_fields(self):
|
||||
return OrderedDict(
|
||||
d = OrderedDict(
|
||||
[
|
||||
('questions',
|
||||
forms.ModelMultipleChoiceField(
|
||||
@@ -69,11 +71,32 @@ class AnswerFilesExporter(BaseExporter):
|
||||
)),
|
||||
]
|
||||
)
|
||||
if self.event.has_subevents:
|
||||
d['subevent'] = forms.ModelChoiceField(
|
||||
label=pgettext_lazy('subevent', 'Date'),
|
||||
queryset=self.event.subevents.all(),
|
||||
required=False,
|
||||
empty_label=pgettext_lazy('subevent', 'All dates')
|
||||
)
|
||||
d['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')
|
||||
}
|
||||
)
|
||||
d['subevent'].widget.choices = d['subevent'].choices
|
||||
return d
|
||||
|
||||
def render(self, form_data: dict):
|
||||
qs = QuestionAnswer.objects.filter(
|
||||
orderposition__order__event=self.event,
|
||||
).select_related('orderposition', 'orderposition__order', 'question')
|
||||
if form_data.get('subevent'):
|
||||
qs = qs.filter(orderposition__subevent=form_data.get('subevent'))
|
||||
if form_data.get('questions'):
|
||||
qs = qs.filter(question__in=form_data['questions'])
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
|
||||
@@ -116,15 +116,29 @@ class DekodiNREIExporter(BaseExporter):
|
||||
'PTNo15': p.full_id or '',
|
||||
})
|
||||
elif p.provider and p.provider.startswith('stripe'):
|
||||
src = p.info_data.get("source", p.info_data)
|
||||
pi = p.info_data or {}
|
||||
try:
|
||||
if "latest_charge" in pi and isinstance(pi.get("latest_charge"), dict):
|
||||
details = pi["latest_charge"]["payment_method_details"]
|
||||
card = details.get("card", {})
|
||||
elif pi.get("charges") and pi["charges"]["data"]:
|
||||
details = pi["charges"]["data"][0].get("payment_method_details", {})
|
||||
card = details.get("card", {})
|
||||
else:
|
||||
details = pi["source"]
|
||||
card = pi["source"]["card"]
|
||||
except:
|
||||
details = {}
|
||||
card = {}
|
||||
|
||||
payments.append({
|
||||
'PTID': '81',
|
||||
'PTN': 'Stripe',
|
||||
'PTNo1': p.info_data.get("id") or '',
|
||||
'PTNo5': src.get("card", {}).get("last4") or '',
|
||||
'PTNo1': pi.get("id") or '',
|
||||
'PTNo5': card.get("last4", ""),
|
||||
'PTNo7': round(float(p.amount), 2) or '',
|
||||
'PTNo8': str(self.event.currency) or '',
|
||||
'PTNo10': src.get('owner', {}).get('verified_name') or src.get('owner', {}).get('name') or '',
|
||||
'PTNo10': details.get('owner', {}).get('verified_name') or details.get('owner', {}).get('name') or '',
|
||||
'PTNo15': p.full_id or '',
|
||||
})
|
||||
else:
|
||||
|
||||
@@ -86,6 +86,7 @@ class InvoiceExporterMixin:
|
||||
('', _('All payment providers')),
|
||||
] + [
|
||||
(k, v.verbose_name) for k, v in self.event.get_payment_providers().items()
|
||||
if not v.is_meta
|
||||
],
|
||||
required=False,
|
||||
help_text=_('Only include invoices for orders that have at least one payment attempt '
|
||||
|
||||
@@ -27,7 +27,6 @@ from openpyxl.styles import Alignment
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
from ...helpers.safe_openpyxl import SafeCell
|
||||
from ..channels import get_all_sales_channels
|
||||
from ..exporter import ListExporter
|
||||
from ..models import ItemMetaValue, ItemVariation, ItemVariationMetaValue
|
||||
from ..signals import register_data_exporters
|
||||
@@ -53,7 +52,7 @@ class ItemDataExporter(ListExporter):
|
||||
|
||||
def iterate_list(self, form_data):
|
||||
locales = self.event.settings.locales
|
||||
scs = get_all_sales_channels()
|
||||
scs = self.organizer.sales_channels.all()
|
||||
header = [
|
||||
_("Product ID"),
|
||||
_("Variation ID"),
|
||||
@@ -141,9 +140,15 @@ class ItemDataExporter(ListExporter):
|
||||
row.append(i.name.localize(l))
|
||||
for l in locales:
|
||||
row.append(v.value.localize(l))
|
||||
|
||||
sales_channels = list(scs)
|
||||
if not i.all_sales_channels:
|
||||
sales_channels = [s for s in sales_channels if s.identifier in (p.identifier for p in i.limit_sales_channels.all())]
|
||||
if not v.all_sales_channels:
|
||||
sales_channels = [s for s in sales_channels if s.identifier in (p.identifier for p in v.limit_sales_channels.all())]
|
||||
row += [
|
||||
_("Yes") if i.active and v.active else "",
|
||||
", ".join([str(sn.verbose_name) for s, sn in scs.items() if s in i.sales_channels and s in v.sales_channels]),
|
||||
", ".join([str(sn.label) for sn in sales_channels]),
|
||||
v.default_price or i.default_price,
|
||||
_("Yes") if i.free_price else "",
|
||||
str(i.tax_rule) if i.tax_rule else "",
|
||||
@@ -186,9 +191,12 @@ class ItemDataExporter(ListExporter):
|
||||
row.append(i.name.localize(l))
|
||||
for l in locales:
|
||||
row.append("")
|
||||
sales_channels = list(scs)
|
||||
if not i.all_sales_channels:
|
||||
sales_channels = [s for s in sales_channels if s.identifier in (p.identifier for p in i.limit_sales_channels.all())]
|
||||
row += [
|
||||
_("Yes") if i.active else "",
|
||||
", ".join([str(sn.verbose_name) for s, sn in scs.items() if s in i.sales_channels]),
|
||||
", ".join([str(sn.label) for sn in sales_channels]),
|
||||
i.default_price,
|
||||
_("Yes") if i.free_price else "",
|
||||
str(i.tax_rule) if i.tax_rule else "",
|
||||
|
||||
@@ -54,6 +54,7 @@ class JSONExporter(BaseExporter):
|
||||
'import in third-party systems.')
|
||||
|
||||
def render(self, form_data):
|
||||
all_sales_channels = self.event.organizer.sales_channels.all()
|
||||
jo = {
|
||||
'event': {
|
||||
'name': str(self.event.name),
|
||||
@@ -85,7 +86,7 @@ class JSONExporter(BaseExporter):
|
||||
'admission': item.admission,
|
||||
'personalized': item.personalized,
|
||||
'active': item.active,
|
||||
'sales_channels': item.sales_channels,
|
||||
'sales_channels': [c.identifier for c in (all_sales_channels if item.all_sales_channels else item.limit_sales_channels.all())],
|
||||
'description': str(item.description),
|
||||
'available_from': item.available_from,
|
||||
'available_until': item.available_until,
|
||||
@@ -114,7 +115,9 @@ class JSONExporter(BaseExporter):
|
||||
'checkin_text': variation.checkin_text,
|
||||
'require_approval': variation.require_approval,
|
||||
'require_membership': variation.require_membership,
|
||||
'sales_channels': variation.sales_channels,
|
||||
'sales_channels': [
|
||||
c.identifier for c in (all_sales_channels if variation.all_sales_channels else variation.limit_sales_channels.all())
|
||||
],
|
||||
'available_from': variation.available_from,
|
||||
'available_until': variation.available_until,
|
||||
'hide_without_voucher': variation.hide_without_voucher,
|
||||
@@ -122,6 +125,7 @@ class JSONExporter(BaseExporter):
|
||||
} for variation in item.variations.all()
|
||||
]
|
||||
} for item in self.event.items.select_related('tax_rule').prefetch_related(
|
||||
'limit_sales_channels',
|
||||
Prefetch(
|
||||
'meta_values',
|
||||
ItemMetaValue.objects.select_related('property'),
|
||||
@@ -130,6 +134,7 @@ class JSONExporter(BaseExporter):
|
||||
Prefetch(
|
||||
'variations',
|
||||
queryset=ItemVariation.objects.prefetch_related(
|
||||
'limit_sales_channels',
|
||||
Prefetch(
|
||||
'meta_values',
|
||||
ItemVariationMetaValue.objects.select_related('property'),
|
||||
@@ -167,7 +172,7 @@ class JSONExporter(BaseExporter):
|
||||
'require_approval': order.require_approval,
|
||||
'checkin_attention': order.checkin_attention,
|
||||
'checkin_text': order.checkin_text,
|
||||
'sales_channel': order.sales_channel,
|
||||
'sales_channel': order.sales_channel.identifier,
|
||||
'expires': order.expires,
|
||||
'datetime': order.datetime,
|
||||
'fees': [
|
||||
|
||||
@@ -32,11 +32,12 @@
|
||||
# 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.
|
||||
|
||||
from collections import OrderedDict
|
||||
from collections import OrderedDict, defaultdict
|
||||
from decimal import Decimal
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.db.models import (
|
||||
Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef,
|
||||
Q, Subquery, Sum, When,
|
||||
@@ -54,7 +55,7 @@ from openpyxl.comments import Comment
|
||||
from openpyxl.styles import Font, PatternFill
|
||||
|
||||
from pretix.base.models import (
|
||||
GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
|
||||
Checkin, GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
|
||||
OrderPosition, Question,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
@@ -541,9 +542,25 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
).order_by()
|
||||
qs = base_qs.annotate(
|
||||
payment_providers=Subquery(p_providers, output_field=CharField()),
|
||||
checked_in_lists=Subquery(
|
||||
Checkin.objects.filter(
|
||||
successful=True,
|
||||
type=Checkin.TYPE_ENTRY,
|
||||
position=OuterRef("pk"),
|
||||
).order_by().values("position").annotate(
|
||||
c=GroupConcat(
|
||||
"list__name",
|
||||
# These appear not to work properly on SQLite. Well, we don't support SQLite outside testing
|
||||
# anyways.
|
||||
ordered='sqlite' not in settings.DATABASES['default']['ENGINE'],
|
||||
distinct='sqlite' not in settings.DATABASES['default']['ENGINE'],
|
||||
delimiter=", "
|
||||
)
|
||||
).values("c")
|
||||
),
|
||||
).select_related(
|
||||
'order', 'order__invoice_address', 'order__customer', 'item', 'variation',
|
||||
'voucher', 'tax_rule'
|
||||
'voucher', 'tax_rule', 'addon_to',
|
||||
).prefetch_related(
|
||||
'subevent', 'subevent__meta_values',
|
||||
'answers', 'answers__question', 'answers__options'
|
||||
@@ -602,13 +619,13 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
_('Valid until'),
|
||||
_('Order comment'),
|
||||
_('Follow-up date'),
|
||||
_('Add-on to position ID'),
|
||||
]
|
||||
|
||||
questions = list(Question.objects.filter(event__in=self.events))
|
||||
options = {}
|
||||
options = defaultdict(list)
|
||||
for q in questions:
|
||||
if q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
options[q.pk] = []
|
||||
if form_data['group_multiple_choice']:
|
||||
for o in q.options.all():
|
||||
options[q.pk].append(o)
|
||||
@@ -618,6 +635,9 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
headers.append(str(q.question) + ' – ' + str(o.answer))
|
||||
options[q.pk].append(o)
|
||||
else:
|
||||
if q.type == Question.TYPE_CHOICE:
|
||||
for o in q.options.all():
|
||||
options[q.pk].append(o)
|
||||
headers.append(str(q.question))
|
||||
headers += [
|
||||
_('Company'),
|
||||
@@ -633,9 +653,11 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
_('VAT ID'),
|
||||
]
|
||||
headers += [
|
||||
_('Sales channel'), _('Order locale'),
|
||||
_('Sales channel'),
|
||||
_('Order locale'),
|
||||
_('E-mail address verified'),
|
||||
_('External customer ID'),
|
||||
_('Check-in lists'),
|
||||
_('Payment providers'),
|
||||
]
|
||||
|
||||
@@ -723,11 +745,12 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
]
|
||||
row.append(order.comment)
|
||||
row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "")
|
||||
row.append(op.addon_to.positionid if op.addon_to_id else "")
|
||||
acache = {}
|
||||
for a in op.answers.all():
|
||||
# We do not want to localize Date, Time and Datetime question answers, as those can lead
|
||||
# to difficulties parsing the data (for example 2019-02-01 may become Février, 2019 01 in French).
|
||||
if a.question.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
if a.question.type in (Question.TYPE_CHOICE_MULTIPLE, Question.TYPE_CHOICE):
|
||||
acache[a.question_id] = set(o.pk for o in a.options.all())
|
||||
elif a.question.type in Question.UNLOCALIZED_TYPES:
|
||||
acache[a.question_id] = a.answer
|
||||
@@ -740,6 +763,10 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
else:
|
||||
for o in options[q.pk]:
|
||||
row.append(_('Yes') if o.pk in acache.get(q.pk, set()) else _('No'))
|
||||
elif q.type == Question.TYPE_CHOICE:
|
||||
# Join is only necessary if the question type was modified but also keeps the code simpler here
|
||||
# as we'd otherwise need some [0] and existance checks
|
||||
row.append(", ".join(str(o.answer) for o in options[q.pk] if o.pk in acache.get(q.pk, set())))
|
||||
else:
|
||||
row.append(acache.get(q.pk, ''))
|
||||
|
||||
@@ -770,6 +797,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
_('Yes') if order.email_known_to_work else _('No'),
|
||||
str(order.customer.external_identifier) if order.customer and order.customer.external_identifier else '',
|
||||
]
|
||||
row.append(op.checked_in_lists or "")
|
||||
row.append(', '.join([
|
||||
str(self.providers.get(p, p)) for p in sorted(set((op.payment_providers or '').split(',')))
|
||||
if p and p != 'free'
|
||||
|
||||
@@ -39,6 +39,7 @@ from django import forms
|
||||
from django.core.validators import URLValidator
|
||||
from django.forms.models import ModelFormMetaclass
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from formtools.wizard.views import SessionWizardView
|
||||
from hierarkey.forms import HierarkeyForm
|
||||
@@ -85,6 +86,43 @@ class I18nInlineFormSet(i18nfield.forms.I18nInlineFormSet):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class MarkdownTextarea(forms.Textarea):
|
||||
|
||||
def _render(self, template_name, context, renderer=None):
|
||||
return mark_safe(
|
||||
'<div class="i18n-form-group">%s<div class="i18n-field-markdown-note">%s</div></div>' % (
|
||||
super()._render(template_name, context, renderer=None),
|
||||
_("You can use {markup_name} in this field.").format(
|
||||
markup_name='<a href="https://docs.pretix.eu/en/latest/user/markdown.html" target="_blank">Markdown</a>'
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class I18nMarkdownTextarea(i18nfield.forms.I18nTextarea):
|
||||
def format_output(self, rendered_widgets) -> str:
|
||||
rendered_widgets = rendered_widgets + [
|
||||
'<div class="i18n-field-markdown-note">%s</div>' % (
|
||||
_("You can use {markup_name} in this field.").format(
|
||||
markup_name='<a href="https://docs.pretix.eu/en/latest/user/markdown.html" target="_blank">Markdown</a>'
|
||||
)
|
||||
)
|
||||
]
|
||||
return super().format_output(rendered_widgets)
|
||||
|
||||
|
||||
class I18nMarkdownTextInput(i18nfield.forms.I18nTextInput):
|
||||
def format_output(self, rendered_widgets) -> str:
|
||||
rendered_widgets = rendered_widgets + [
|
||||
'<div class="i18n-field-markdown-note">%s</div>' % (
|
||||
_("You can use {markup_name} in this field.").format(
|
||||
markup_name='<a href="https://docs.pretix.eu/en/latest/user/markdown.html" target="_blank">Markdown</a>'
|
||||
)
|
||||
)
|
||||
]
|
||||
return super().format_output(rendered_widgets)
|
||||
|
||||
|
||||
SECRET_REDACTED = '*****'
|
||||
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
|
||||
import hashlib
|
||||
import ipaddress
|
||||
import logging
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
@@ -44,10 +45,13 @@ from django.contrib.auth.password_validation import (
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.metrics import pretix_failed_logins
|
||||
from pretix.base.models import User
|
||||
from pretix.helpers.dicts import move_to_end
|
||||
from pretix.helpers.http import get_client_ip
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LoginForm(forms.Form):
|
||||
"""
|
||||
@@ -55,6 +59,7 @@ class LoginForm(forms.Form):
|
||||
username/password logins.
|
||||
"""
|
||||
keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False)
|
||||
origin = forms.CharField(widget=forms.HiddenInput, required=False)
|
||||
|
||||
error_messages = {
|
||||
'invalid_login': _("This combination of credentials is not known to our system."),
|
||||
@@ -104,12 +109,16 @@ class LoginForm(forms.Form):
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.get(self.ratelimit_key)
|
||||
if cnt and int(cnt) > 10:
|
||||
pretix_failed_logins.inc(1, reason="ratelimit")
|
||||
logger.info("Backend login rejected due to rate limit.")
|
||||
raise forms.ValidationError(self.error_messages['rate_limit'], code='rate_limit')
|
||||
self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
|
||||
if self.user_cache is None:
|
||||
if self.ratelimit_key:
|
||||
rc.incr(self.ratelimit_key)
|
||||
rc.expire(self.ratelimit_key, 300)
|
||||
logger.info("Backend login invalid.")
|
||||
pretix_failed_logins.inc(1, reason="invalid")
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['invalid_login'],
|
||||
code='invalid_login'
|
||||
@@ -131,6 +140,8 @@ class LoginForm(forms.Form):
|
||||
If the given user may log in, this method should return None.
|
||||
"""
|
||||
if not user.is_active:
|
||||
logger.info("Backend login rejected due to user inactive.")
|
||||
pretix_failed_logins.inc(1, reason="inactive")
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['inactive'],
|
||||
code='inactive',
|
||||
|
||||
@@ -57,7 +57,7 @@ from django.forms.widgets import FILE_INPUT_CONTRADICTION
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import get_current_timezone, now
|
||||
from django.utils.timezone import get_current_timezone
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_countries import countries
|
||||
from django_countries.fields import Country, CountryField
|
||||
@@ -86,6 +86,7 @@ from pretix.base.settings import (
|
||||
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
|
||||
)
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
from pretix.base.timemachine import time_machine_now
|
||||
from pretix.control.forms import (
|
||||
ExtFileField, ExtValidationMixin, SizeValidationMixin, SplitDateTimeField,
|
||||
)
|
||||
@@ -606,30 +607,41 @@ class BaseQuestionsForm(forms.Form):
|
||||
|
||||
if cartpos and item.validity_mode == Item.VALIDITY_MODE_DYNAMIC and item.validity_dynamic_start_choice:
|
||||
if item.validity_dynamic_start_choice_day_limit:
|
||||
max_date = now().astimezone(event.timezone) + timedelta(days=item.validity_dynamic_start_choice_day_limit)
|
||||
max_date = time_machine_now().astimezone(event.timezone) + timedelta(days=item.validity_dynamic_start_choice_day_limit)
|
||||
else:
|
||||
max_date = None
|
||||
min_date = time_machine_now()
|
||||
initial = None
|
||||
if (item.require_membership or (pos.variation and pos.variation.require_membership)) and pos.used_membership:
|
||||
if pos.used_membership.date_start >= time_machine_now():
|
||||
initial = min_date = pos.used_membership.date_start
|
||||
max_date = min(max_date, pos.used_membership.date_end) if max_date else pos.used_membership.date_end
|
||||
if item.validity_dynamic_duration_months or item.validity_dynamic_duration_days:
|
||||
attrs = {}
|
||||
if max_date:
|
||||
attrs['data-max'] = max_date.date().isoformat()
|
||||
if min_date:
|
||||
attrs['data-min'] = min_date.date().isoformat()
|
||||
self.fields['requested_valid_from'] = forms.DateField(
|
||||
label=_('Start date'),
|
||||
help_text=_('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
|
||||
required=False,
|
||||
help_text='' if initial else _('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
|
||||
required=bool(initial),
|
||||
initial=pos.requested_valid_from or initial,
|
||||
widget=DatePickerWidget(attrs),
|
||||
validators=[MaxDateValidator(max_date.date())] if max_date else []
|
||||
validators=([MaxDateValidator(max_date.date())] if max_date else []) + [MinDateValidator(min_date.date())]
|
||||
)
|
||||
else:
|
||||
self.fields['requested_valid_from'] = forms.SplitDateTimeField(
|
||||
label=_('Start date'),
|
||||
help_text=_('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
|
||||
required=False,
|
||||
help_text='' if initial else _('If you keep this empty, the ticket will be valid starting at the time of purchase.'),
|
||||
required=bool(initial),
|
||||
initial=pos.requested_valid_from or initial,
|
||||
widget=SplitDateTimePickerWidget(
|
||||
time_format=get_format_without_seconds('TIME_INPUT_FORMATS'),
|
||||
min_date=min_date,
|
||||
max_date=max_date
|
||||
),
|
||||
validators=[MaxDateTimeValidator(max_date)] if max_date else []
|
||||
validators=([MaxDateTimeValidator(max_date)] if max_date else []) + [MinDateTimeValidator(min_date)]
|
||||
)
|
||||
|
||||
add_fields = {}
|
||||
@@ -1023,7 +1035,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
self.all_optional = kwargs.pop('all_optional', False)
|
||||
|
||||
kwargs.setdefault('initial', {})
|
||||
if not kwargs.get('instance') or not kwargs['instance'].country:
|
||||
if (not kwargs.get('instance') or not kwargs['instance'].country) and not kwargs["initial"].get("country"):
|
||||
kwargs['initial']['country'] = guess_country_from_request(self.request, self.event)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -1159,7 +1171,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
self.instance.vat_id_validated = False
|
||||
messages.warning(self.request, e.message)
|
||||
else:
|
||||
raise ValidationError(e.message)
|
||||
raise ValidationError({"vat_id": e.message})
|
||||
except VATIDTemporaryError as e:
|
||||
self.instance.vat_id_validated = False
|
||||
if self.request and self.vat_warning:
|
||||
|
||||
@@ -32,13 +32,13 @@
|
||||
# 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 re
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import BaseValidator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.helpers.format import format_map
|
||||
|
||||
|
||||
class PlaceholderValidator(BaseValidator):
|
||||
"""
|
||||
@@ -47,6 +47,12 @@ class PlaceholderValidator(BaseValidator):
|
||||
which are not presented in taken list.
|
||||
"""
|
||||
|
||||
error_message = _(
|
||||
'There is an error with your placeholder syntax. Please check that the opening "{" and closing "}" curly '
|
||||
'brackets on your placeholders match up. '
|
||||
'Please note: to use literal "{" or "}", you need to double them as "{{" and "}}".'
|
||||
)
|
||||
|
||||
def __init__(self, limit_value):
|
||||
super().__init__(limit_value)
|
||||
self.limit_value = limit_value
|
||||
@@ -57,22 +63,15 @@ class PlaceholderValidator(BaseValidator):
|
||||
self.__call__(v)
|
||||
return
|
||||
|
||||
if value.count('{') != value.count('}'):
|
||||
try:
|
||||
format_map(value, {key.strip('{}'): "" for key in self.limit_value}, raise_on_missing=True)
|
||||
except ValueError:
|
||||
raise ValidationError(self.error_message, code='invalid_placeholder_syntax')
|
||||
except KeyError as e:
|
||||
raise ValidationError(
|
||||
_('Invalid placeholder syntax: You used a different number of "{" than of "}".'),
|
||||
code='invalid_placeholder_syntax',
|
||||
)
|
||||
|
||||
data_placeholders = list(re.findall(r'({[^}]*})', value, re.X))
|
||||
invalid_placeholders = []
|
||||
for placeholder in data_placeholders:
|
||||
if placeholder not in self.limit_value:
|
||||
invalid_placeholders.append(placeholder)
|
||||
if invalid_placeholders:
|
||||
raise ValidationError(
|
||||
_('Invalid placeholder(s): %(value)s'),
|
||||
_('Invalid placeholder: {%(value)s}'),
|
||||
code='invalid_placeholders',
|
||||
params={'value': ", ".join(invalid_placeholders,)})
|
||||
params={'value': e.args[0]})
|
||||
|
||||
def clean(self, x):
|
||||
return x
|
||||
|
||||
@@ -33,11 +33,12 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import os
|
||||
from datetime import date
|
||||
from datetime import datetime
|
||||
|
||||
from django import forms
|
||||
from django.utils.formats import get_format
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.html import escape
|
||||
from django.utils.timezone import get_current_timezone, now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -64,7 +65,7 @@ def format_placeholders_help_text(placeholders, event=None):
|
||||
placeholders = [(k, v.render_sample(event) if event else v) for k, v in placeholders.items()]
|
||||
placeholders.sort(key=lambda x: x[0])
|
||||
phs = [
|
||||
'<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (_("Sample: %s") % v if v else "", k)
|
||||
'<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (escape(_("Sample: %s") % v) if v else "", escape(k))
|
||||
for k, v in placeholders
|
||||
]
|
||||
return _('Available placeholders: {list}').format(
|
||||
@@ -188,11 +189,11 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
||||
time_attrs['autocomplete'] = 'off'
|
||||
if min_date:
|
||||
date_attrs['data-min'] = (
|
||||
min_date if isinstance(min_date, date) else min_date.astimezone(get_current_timezone()).date()
|
||||
min_date if not isinstance(min_date, datetime) else min_date.astimezone(get_current_timezone()).date()
|
||||
).isoformat()
|
||||
if max_date:
|
||||
date_attrs['data-max'] = (
|
||||
max_date if isinstance(max_date, date) else max_date.astimezone(get_current_timezone()).date()
|
||||
max_date if not isinstance(max_date, datetime) else max_date.astimezone(get_current_timezone()).date()
|
||||
).isoformat()
|
||||
|
||||
def date_placeholder():
|
||||
|
||||
@@ -30,7 +30,7 @@ from typing import Tuple
|
||||
|
||||
import bleach
|
||||
import vat_moss.exchange_rates
|
||||
from bidi.algorithm import get_display
|
||||
from bidi import get_display
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.db.models import Sum
|
||||
from django.dispatch import receiver
|
||||
@@ -182,7 +182,7 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
||||
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
|
||||
italic='OpenSansIt', boldItalic='OpenSansBI')
|
||||
|
||||
for family, styles in get_fonts().items():
|
||||
for family, styles in get_fonts(event=self.event, pdf_support_required=True).items():
|
||||
if family == self.event.settings.invoice_renderer_font:
|
||||
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
|
||||
self.font_regular = family
|
||||
@@ -625,7 +625,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
)]
|
||||
else:
|
||||
tdata = [(
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['BoldRight']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
|
||||
Paragraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']),
|
||||
)]
|
||||
|
||||
@@ -268,7 +268,10 @@ def metric_values():
|
||||
dkey = key.decode("utf-8")
|
||||
splitted = dkey.split("{", 2)
|
||||
value = float(value.decode("utf-8"))
|
||||
metrics[splitted[0]]["{" + splitted[1]] = value
|
||||
if len(splitted) == 1:
|
||||
metrics[splitted[0]][""] = value
|
||||
else:
|
||||
metrics[splitted[0]]["{" + splitted[1]] = value
|
||||
|
||||
# Aliases
|
||||
aliases = {
|
||||
@@ -314,3 +317,5 @@ pretix_task_runs_total = Counter("pretix_task_runs_total", "Total calls to a cel
|
||||
["task_name", "status"])
|
||||
pretix_task_duration_seconds = Histogram("pretix_task_duration_seconds", "Call time of a celery task",
|
||||
["task_name"])
|
||||
pretix_successful_logins = Counter("pretix_logins_successful", "Successful logins", [])
|
||||
pretix_failed_logins = Counter("pretix_logins_failed", "Failed logins", ["reason"])
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from collections import OrderedDict
|
||||
from urllib.parse import urlsplit
|
||||
from urllib.parse import urlparse, urlsplit
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
from django.conf import settings
|
||||
@@ -40,6 +40,7 @@ from pretix.base.settings import global_settings_object
|
||||
from pretix.multidomain.urlreverse import (
|
||||
get_event_domain, get_organizer_domain,
|
||||
)
|
||||
from pretix.presale.style import get_fonts
|
||||
|
||||
_supported = None
|
||||
|
||||
@@ -240,6 +241,14 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
)
|
||||
|
||||
def process_response(self, request, resp):
|
||||
def nested_dict_values(d):
|
||||
for v in d.values():
|
||||
if isinstance(v, dict):
|
||||
yield from nested_dict_values(v)
|
||||
else:
|
||||
if isinstance(v, str):
|
||||
yield v
|
||||
|
||||
url = resolve(request.path_info)
|
||||
|
||||
if settings.DEBUG and resp.status_code >= 400:
|
||||
@@ -247,8 +256,6 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
# pages
|
||||
return resp
|
||||
|
||||
resp['X-XSS-Protection'] = '1'
|
||||
|
||||
# We just need to have a P3P, not matter whats in there
|
||||
# https://blogs.msdn.microsoft.com/ieinternals/2013/09/17/a-quick-look-at-p3p/
|
||||
# https://github.com/pretix/pretix/issues/765
|
||||
@@ -259,6 +266,14 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
if gs.settings.leaflet_tiles:
|
||||
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
|
||||
|
||||
font_src = set()
|
||||
if hasattr(request, 'event'):
|
||||
for font in get_fonts(request.event, pdf_support_required=False).values():
|
||||
for path in list(nested_dict_values(font)):
|
||||
font_location = urlparse(path)
|
||||
if font_location.scheme and font_location.netloc:
|
||||
font_src.add('{}://{}'.format(font_location.scheme, font_location.netloc))
|
||||
|
||||
h = {
|
||||
'default-src': ["{static}"],
|
||||
'script-src': ['{static}'],
|
||||
@@ -267,7 +282,7 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
'style-src': ["{static}", "{media}"],
|
||||
'connect-src': ["{dynamic}", "{media}"],
|
||||
'img-src': ["{static}", "{media}", "data:"] + img_src,
|
||||
'font-src': ["{static}"],
|
||||
'font-src': ["{static}"] + list(font_src),
|
||||
'media-src': ["{static}", "data:"],
|
||||
# form-action is not only used to match on form actions, but also on URLs
|
||||
# form-actions redirect to. In the context of e.g. payment providers or
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.channels import get_all_sales_channel_types
|
||||
|
||||
|
||||
def set_sales_channels(apps, schema_editor):
|
||||
@@ -11,7 +11,7 @@ def set_sales_channels(apps, schema_editor):
|
||||
# Therefore, for existing events, we enable all sales channels
|
||||
Event_SettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
|
||||
Event = apps.get_model('pretixbase', 'Event')
|
||||
all_sales_channels = "[" + ", ".join('"' + sc + '"' for sc in get_all_sales_channels()) + "]"
|
||||
all_sales_channels = "[" + ", ".join('"' + sc + '"' for sc in get_all_sales_channel_types()) + "]"
|
||||
batch_size = 1000
|
||||
Event_SettingsStore.objects.bulk_create([
|
||||
Event_SettingsStore(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from django.db import migrations
|
||||
|
||||
import pretix.base.models.fields
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.channels import get_all_sales_channel_types
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -15,6 +15,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='sales_channels',
|
||||
field=pretix.base.models.fields.MultiStringField(default=list(get_all_sales_channels().keys())),
|
||||
field=pretix.base.models.fields.MultiStringField(default=list(get_all_sales_channel_types().keys())),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.2.9 on 2024-01-30 11:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0256_itemvariation_unavail_modes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="item",
|
||||
name="default_price",
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=13),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
48
src/pretix/base/migrations/0258_uniq_indx.py
Normal file
48
src/pretix/base/migrations/0258_uniq_indx.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# Generated by Django 4.2.10 on 2024-03-15 09:59
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0257_item_default_price_not_null"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="order",
|
||||
name="organizer",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="orders",
|
||||
to="pretixbase.organizer",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="orderposition",
|
||||
name="organizer",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="order_positions",
|
||||
to="pretixbase.organizer",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="order",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("organizer", "code"), name="order_organizer_code_uniq"
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="orderposition",
|
||||
constraint=models.UniqueConstraint(
|
||||
models.F("organizer"),
|
||||
models.F("secret"),
|
||||
name="orderposition_organizer_secret_uniq",
|
||||
),
|
||||
),
|
||||
]
|
||||
18
src/pretix/base/migrations/0259_team_require_2fa.py
Normal file
18
src/pretix/base/migrations/0259_team_require_2fa.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.10 on 2024-04-02 11:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0258_uniq_indx"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="team",
|
||||
name="require_2fa",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.10 on 2024-04-02 15:16
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0259_team_require_2fa"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterIndexTogether(
|
||||
name="reusablemedium",
|
||||
index_together=set(),
|
||||
),
|
||||
]
|
||||
48
src/pretix/base/migrations/0261_userknownloginsource.py
Normal file
48
src/pretix/base/migrations/0261_userknownloginsource.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# Generated by Django 4.2.10 on 2024-04-02 15:37
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.helpers.countries
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0260_alter_reusablemedium_index_together"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="UserKnownLoginSource",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
("agent_type", models.CharField(max_length=255, null=True)),
|
||||
("device_type", models.CharField(max_length=255, null=True)),
|
||||
("os_type", models.CharField(max_length=255, null=True)),
|
||||
(
|
||||
"country",
|
||||
pretix.helpers.countries.FastCountryField(
|
||||
countries=pretix.helpers.countries.CachedCountries,
|
||||
max_length=2,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
("last_seen", models.DateTimeField()),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="known_login_sources",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
18
src/pretix/base/migrations/0262_subevent_comment.py
Normal file
18
src/pretix/base/migrations/0262_subevent_comment.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.10 on 2024-04-19 14:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0261_userknownloginsource"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="subevent",
|
||||
name="comment",
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
]
|
||||
26
src/pretix/base/migrations/0263_auto_20240409_0732.py
Normal file
26
src/pretix/base/migrations/0263_auto_20240409_0732.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 4.2.10 on 2024-04-09 07:32
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def change_currencies(apps, schema_editor):
|
||||
Event = apps.get_model("pretixbase", "Event")
|
||||
Event.objects.filter(
|
||||
currency__in={
|
||||
'XAG', 'XAU', 'XBA', 'XBB', 'XBC', 'XBD', 'XDR', 'XPD', 'XPT', 'XSU', 'XTS', 'XUA',
|
||||
}
|
||||
).update(currency='XXX')
|
||||
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0262_subevent_comment"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
change_currencies, migrations.RunPython.noop
|
||||
)
|
||||
]
|
||||
24
src/pretix/base/migrations/0264_order_internal_secret.py
Normal file
24
src/pretix/base/migrations/0264_order_internal_secret.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 4.2.11 on 2024-05-16 11:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.orders
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0263_auto_20240409_0732"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="order",
|
||||
name="internal_secret",
|
||||
field=models.CharField(
|
||||
default=None,
|
||||
max_length=32,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
110
src/pretix/base/migrations/0265_saleschannel_and_more.py
Normal file
110
src/pretix/base/migrations/0265_saleschannel_and_more.py
Normal file
@@ -0,0 +1,110 @@
|
||||
# Generated by Django 4.2.8 on 2024-03-24 17:43
|
||||
|
||||
import django.db.models.deletion
|
||||
import i18nfield.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0264_order_internal_secret"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="SalesChannel",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
("label", i18nfield.fields.I18nCharField(max_length=200)),
|
||||
("identifier", models.CharField(max_length=200)),
|
||||
("type", models.CharField(max_length=200)),
|
||||
("position", models.PositiveIntegerField(default=0)),
|
||||
("configuration", models.JSONField(default=dict)),
|
||||
],
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="checkinlist",
|
||||
old_name="auto_checkin_sales_channels",
|
||||
new_name="auto_checkin_sales_channel_types",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="discount",
|
||||
name="all_sales_channels",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="event",
|
||||
name="all_sales_channels",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="item",
|
||||
name="all_sales_channels",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="itemvariation",
|
||||
name="all_sales_channels",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="order",
|
||||
old_name="sales_channel",
|
||||
new_name="sales_channel_type",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="saleschannel",
|
||||
name="organizer",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="sales_channels",
|
||||
to="pretixbase.organizer",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="discount",
|
||||
name="limit_sales_channels",
|
||||
field=models.ManyToManyField(to="pretixbase.saleschannel"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="event",
|
||||
name="limit_sales_channels",
|
||||
field=models.ManyToManyField(to="pretixbase.saleschannel"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="item",
|
||||
name="limit_sales_channels",
|
||||
field=models.ManyToManyField(to="pretixbase.saleschannel"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="itemvariation",
|
||||
name="limit_sales_channels",
|
||||
field=models.ManyToManyField(to="pretixbase.saleschannel"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="checkinlist",
|
||||
name="auto_checkin_sales_channels",
|
||||
field=models.ManyToManyField(to="pretixbase.saleschannel"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="order",
|
||||
name="sales_channel",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="pretixbase.saleschannel",
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="saleschannel",
|
||||
unique_together={("organizer", "identifier")},
|
||||
),
|
||||
]
|
||||
84
src/pretix/base/migrations/0266_saleschannel_migrate_data.py
Normal file
84
src/pretix/base/migrations/0266_saleschannel_migrate_data.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# Generated by Django 4.2.8 on 2024-03-24 17:55
|
||||
|
||||
from django.db import migrations
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.channels import get_all_sales_channel_types
|
||||
|
||||
|
||||
def create_sales_channels(apps, schema_editor):
|
||||
channel_types = get_all_sales_channel_types()
|
||||
type_to_channel = dict()
|
||||
full_discount_set = set()
|
||||
full_set = set()
|
||||
|
||||
Organizer = apps.get_model("pretixbase", "Organizer")
|
||||
for o in Organizer.objects.all().iterator():
|
||||
for i, t in enumerate(channel_types.values()):
|
||||
if not t.default_created:
|
||||
continue
|
||||
type_to_channel[t.identifier, o.pk] = o.sales_channels.get_or_create(
|
||||
type=t.identifier,
|
||||
defaults=dict(
|
||||
position=i,
|
||||
identifier=t.identifier,
|
||||
label=LazyI18nString.from_gettext(t.verbose_name),
|
||||
),
|
||||
)[0]
|
||||
full_set.add(t.identifier)
|
||||
if t.discounts_supported:
|
||||
full_discount_set.add(t.identifier)
|
||||
|
||||
Event = apps.get_model("pretixbase", "Event")
|
||||
for d in Event.objects.all().iterator():
|
||||
if set(d.sales_channels) != full_set:
|
||||
d.all_sales_channels = False
|
||||
d.save()
|
||||
for s in d.sales_channels:
|
||||
d.limit_sales_channels.add(type_to_channel[s, d.organizer_id])
|
||||
|
||||
Item = apps.get_model("pretixbase", "Item")
|
||||
for d in Item.objects.select_related("event").iterator():
|
||||
if set(d.sales_channels) != full_set:
|
||||
d.all_sales_channels = False
|
||||
d.save()
|
||||
for s in d.sales_channels:
|
||||
d.limit_sales_channels.add(type_to_channel[s, d.event.organizer_id])
|
||||
|
||||
ItemVariation = apps.get_model("pretixbase", "ItemVariation")
|
||||
for d in ItemVariation.objects.select_related("item__event").iterator():
|
||||
if set(d.sales_channels) != full_set:
|
||||
d.all_sales_channels = False
|
||||
d.save()
|
||||
for s in d.sales_channels:
|
||||
d.limit_sales_channels.add(type_to_channel[s, d.item.event.organizer_id])
|
||||
|
||||
Discount = apps.get_model("pretixbase", "Discount")
|
||||
for d in Discount.objects.select_related("event").iterator():
|
||||
if set(d.sales_channels) != full_discount_set:
|
||||
d.all_sales_channels = False
|
||||
d.save()
|
||||
for s in d.sales_channels:
|
||||
d.limit_sales_channels.add(type_to_channel[s, d.event.organizer_id])
|
||||
|
||||
CheckinList = apps.get_model("pretixbase", "CheckinList")
|
||||
for c in CheckinList.objects.select_related("event").iterator():
|
||||
for s in c.auto_checkin_sales_channel_types:
|
||||
c.auto_checkin_sales_channels.add(type_to_channel[s, c.event.organizer_id])
|
||||
|
||||
Order = apps.get_model("pretixbase", "Order")
|
||||
for (k, orgid), v in type_to_channel.items():
|
||||
Order.objects.filter(sales_channel_type=k, event__organizer_id=orgid, sales_channel__isnull=True).update(
|
||||
sales_channel=v
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0265_saleschannel_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_sales_channels, migrations.RunPython.noop),
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user