Compare commits

..
Author SHA1 Message Date
Mira Weller b23fdf2000 rename 2024-10-25 18:01:11 +02:00
MiraandGitHub 80d1dd16f4 Merge branch 'master' into hacky-debug-output 2024-10-25 17:57:06 +02:00
dependabot[bot]Mira Wellerdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
82ed8abecc Update fakeredis requirement from ==2.24.* to ==2.26.* (#4551)
Updates the requirements on [fakeredis](https://github.com/cunla/fakeredis-py) to permit the latest version.
- [Release notes](https://github.com/cunla/fakeredis-py/releases)
- [Commits](https://github.com/cunla/fakeredis-py/compare/v2.24.0...v2.26.0)

---
updated-dependencies:
- dependency-name: fakeredis
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-25 17:55:13 +02:00
Mira Weller c0849c9d9c disable reruns 2024-10-25 17:33:36 +02:00
Raphael MichelandMira Weller e3f6b3f70a pytest: Fix failure with fakeserver and xdist 2024-10-25 17:30:47 +02:00
MiraandGitHub 67934e51fc Merge branch 'master' into hacky-debug-output 2024-10-25 17:08:02 +02:00
Mira Weller d4ea0990b3 readd matrix 2024-10-25 17:03:55 +02:00
Raphael MichelandMira Weller 9b4506d5e5 Speed up process with uv 2024-10-25 17:02:22 +02:00
Mira Weller f559cd2618 flush, always print 2024-10-25 17:01:33 +02:00
Mira Weller 33991cfb77 add some hacky debug output 2024-10-25 16:43:21 +02:00
Mira Weller fe12eea591 Revert "Update fakeredis requirement from ==2.24.* to ==2.26.* (#4551)"
This reverts commit cf3087453c.
2024-10-25 16:06:21 +02:00
Mira Weller 7000d4897d Revert "Fix fakeredis usage"
This reverts commit 39c3aef7bc.
2024-10-25 16:06:20 +02:00
Mira Weller e6337e07e1 Revert "pytest: Fix failure with fakeserver and xdist"
This reverts commit b46c0eba0c.
2024-10-25 16:06:19 +02:00
93 changed files with 27411 additions and 30942 deletions
+13 -6
View File
@@ -22,9 +22,10 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
name: Tests name: Tests
strategy: strategy:
fail-fast: false
matrix: matrix:
python-version: ["3.9", "3.10", "3.11"] python-version: ["3.9", "3.10", "3.11"]
database: [sqlite, postgres] database: [ postgres]
exclude: exclude:
- database: sqlite - database: sqlite
python-version: "3.9" python-version: "3.9"
@@ -37,7 +38,7 @@ jobs:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_DB: pretix POSTGRES_DB: pretix
options: >- options: >-
--health-cmd "pg_isready -U postgres -d pretix" --health-cmd pg_isready
--health-interval 10s --health-interval 10s
--health-timeout 5s --health-timeout 5s
--health-retries 5 --health-retries 5
@@ -70,15 +71,21 @@ jobs:
run: make all compress run: make all compress
- name: Run tests - name: Run tests
working-directory: ./src working-directory: ./src
run: PRETIX_CONFIG_FILE=tests/ci_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml tests --maxfail=100 run: PRETIX_CONFIG_FILE=tests/ci_${{ matrix.database }}.cfg py.test -n 3 -p no:sugar --cov=./ --cov-report=xml --reruns 0 tests --maxfail=100 || true
# XXXXXXXXXXXXXX for test only
- name: print debug output
working-directory: ./src
run: cat /tmp/test.txt
- name: Run concurrency tests - name: Run concurrency tests
working-directory: ./src working-directory: ./src
run: PRETIX_CONFIG_FILE=tests/ci_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reuse-db run: PRETIX_CONFIG_FILE=tests/ci_${{ matrix.database }}.cfg py.test tests/concurrency_tests/ --reruns 0 --reuse-db
if: matrix.database == 'postgres' if: matrix.database == 'postgres'
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v1
with: with:
file: src/coverage.xml file: src/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false fail_ci_if_error: true
if: matrix.database == 'postgres' && matrix.python-version == '3.11' if: matrix.database == 'postgres' && matrix.python-version == '3.11'
+1 -1
View File
@@ -10,7 +10,7 @@ tests:
- cd src - cd src
- python manage.py check - python manage.py check
- make all compress - make all compress
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --maxfail=100 - PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test --reruns 3 -n 3 tests --maxfail=100
except: except:
- pypi - pypi
pypi: pypi:
+5 -5
View File
@@ -71,7 +71,7 @@ Endpoints
"mode": "placed", "mode": "placed",
"all_sales_channels": false, "all_sales_channels": false,
"limit_sales_channels": ["web"], "limit_sales_channels": ["web"],
"all_products": false, "all_products": False,
"limit_products": [2, 3], "limit_products": [2, 3],
"limit_variations": [456], "limit_variations": [456],
"all_payment_methods": true, "all_payment_methods": true,
@@ -113,7 +113,7 @@ Endpoints
"mode": "placed", "mode": "placed",
"all_sales_channels": false, "all_sales_channels": false,
"limit_sales_channels": ["web"], "limit_sales_channels": ["web"],
"all_products": false, "all_products": False,
"limit_products": [2, 3], "limit_products": [2, 3],
"limit_variations": [456], "limit_variations": [456],
"all_payment_methods": true, "all_payment_methods": true,
@@ -146,7 +146,7 @@ Endpoints
"mode": "placed", "mode": "placed",
"all_sales_channels": false, "all_sales_channels": false,
"limit_sales_channels": ["web"], "limit_sales_channels": ["web"],
"all_products": false, "all_products": False,
"limit_products": [2, 3], "limit_products": [2, 3],
"limit_variations": [456], "limit_variations": [456],
"all_payment_methods": true, "all_payment_methods": true,
@@ -167,7 +167,7 @@ Endpoints
"mode": "placed", "mode": "placed",
"all_sales_channels": false, "all_sales_channels": false,
"limit_sales_channels": ["web"], "limit_sales_channels": ["web"],
"all_products": false, "all_products": False,
"limit_products": [2, 3], "limit_products": [2, 3],
"limit_variations": [456], "limit_variations": [456],
"all_payment_methods": true, "all_payment_methods": true,
@@ -216,7 +216,7 @@ Endpoints
"mode": "placed", "mode": "placed",
"all_sales_channels": false, "all_sales_channels": false,
"limit_sales_channels": ["web"], "limit_sales_channels": ["web"],
"all_products": false, "all_products": False,
"limit_products": [2, 3], "limit_products": [2, 3],
"limit_variations": [456], "limit_variations": [456],
"all_payment_methods": true, "all_payment_methods": true,
+2 -121
View File
@@ -206,17 +206,6 @@ checkins list of objects List of **succe
├ device integer Internal ID of the device. Can be ``null``. **Deprecated**, since this ID is not otherwise used in the API and is therefore not very useful. ├ device integer Internal ID of the device. Can be ``null``. **Deprecated**, since this ID is not otherwise used in the API and is therefore not very useful.
├ device_id integer Attribute ``device_id`` of the device. Can be ``null``. ├ device_id integer Attribute ``device_id`` of the device. Can be ``null``.
└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system └ auto_checked_in boolean Indicates if this check-in been performed automatically by the system
print_logs list of objects List of print jobs recorded e.g. by the pretix apps
├ id integer Internal ID of the print job
├ successful boolean Whether the print job successfully resulted in a print.
This is not expected to be 100 % reliable information (since
printer feedback is never perfect) and there is no guarantee
that unsuccessful jobs will be logged.
├ device_id integer Attribute ``device_id`` of the device that recorded the print. Can be ``null``.
├ datetime datetime Time of printing
├ source string Source of print job, e.g. name of the app used.
├ type string Type of print (currently ``badge``, ``ticket``, ``certificate``, or ``other``)
└ info object Additional data with client-dependent structure.
downloads list of objects List of ticket download options downloads list of objects List of ticket download options
├ output string Ticket output provider (e.g. ``pdf``, ``passbook``) ├ output string Ticket output provider (e.g. ``pdf``, ``passbook``)
└ url string Download URL └ url string Download URL
@@ -244,10 +233,6 @@ pdf_data object Data object req
The attributes ``blocked``, ``valid_from`` and ``valid_until`` have been added. The attributes ``blocked``, ``valid_from`` and ``valid_until`` have been added.
.. versionchanged:: 2024.9
The attribute ``print_logs`` has been added.
.. _order-payment-resource: .. _order-payment-resource:
Order payment resource Order payment resource
@@ -414,21 +399,10 @@ List of all orders
"type": "entry", "type": "entry",
"gate": null, "gate": null,
"device": 2, "device": 2,
"device_id": 1,
"datetime": "2017-12-25T12:45:23Z", "datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false "auto_checked_in": false
} }
], ],
"print_logs": [
{
"id": 1,
"type": "badge",
"datetime": "2017-12-25T12:45:23Z",
"device_id": 1,
"source": "pretixSCAN",
"info": {}
}
],
"answers": [ "answers": [
{ {
"question": 12, "question": 12,
@@ -652,22 +626,10 @@ Fetching individual orders
"type": "entry", "type": "entry",
"gate": null, "gate": null,
"device": 2, "device": 2,
"device_id": 1,
"datetime": "2017-12-25T12:45:23Z", "datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false "auto_checked_in": false
} }
], ],
"print_logs": [
{
"id": 1,
"type": "badge",
"successful": true,
"datetime": "2017-12-25T12:45:23Z",
"device_id": 1,
"source": "pretixSCAN",
"info": {}
}
],
"answers": [ "answers": [
{ {
"question": 12, "question": 12,
@@ -1015,8 +977,8 @@ Creating orders
* ``internal_reference`` * ``internal_reference``
* ``vat_id`` * ``vat_id``
* ``vat_id_validated`` (optional) If you need support for reverse charge (rarely the case), you need to check * ``vat_id_validated`` (optional) If you need support for reverse charge (rarely the case), you need to check
yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
trigger reverse charge taxation. Don't forget to set ``is_business`` as well! trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
* ``positions`` * ``positions``
@@ -1619,22 +1581,10 @@ List of all order positions
"type": "entry", "type": "entry",
"gate": null, "gate": null,
"device": 2, "device": 2,
"device_id": 1,
"datetime": "2017-12-25T12:45:23Z", "datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false "auto_checked_in": false
} }
], ],
"print_logs": [
{
"id": 1,
"type": "badge",
"successful": true,
"datetime": "2017-12-25T12:45:23Z",
"device_id": 1,
"source": "pretixSCAN",
"info": {}
}
],
"answers": [ "answers": [
{ {
"question": 12, "question": 12,
@@ -1745,22 +1695,10 @@ Fetching individual positions
"type": "entry", "type": "entry",
"gate": null, "gate": null,
"device": 2, "device": 2,
"device_id": 1,
"datetime": "2017-12-25T12:45:23Z", "datetime": "2017-12-25T12:45:23Z",
"auto_checked_in": false "auto_checked_in": false
} }
], ],
"print_logs": [
{
"id": 1,
"type": "badge",
"successful": true,
"datetime": "2017-12-25T12:45:23Z",
"device_id": 1,
"source": "pretixSCAN",
"info": {}
}
],
"answers": [ "answers": [
{ {
"question": 12, "question": 12,
@@ -1857,10 +1795,6 @@ Manipulating individual positions
The endpoints to manage blocks have been added. The endpoints to manage blocks have been added.
.. versionchanged:: 2024.9
The API now supports logging ticket and badge prints.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/ .. http:patch:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/
Updates specific fields on an order position. Currently, only the following fields are supported: Updates specific fields on an order position. Currently, only the following fields are supported:
@@ -2120,59 +2054,6 @@ Manipulating individual positions
:statuscode 401: Authentication failure :statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order position. :statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order position.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/printlog/
Creates a print log, stating that this ticket has been printed.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/printlog/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"datetime": "2024-09-19T13:37:00+02:00",
"source": "pretixPOS",
"type": "badge",
"info": {
"cashier": 1234
}
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/pdf
{
"id": 1234,
"device_id": null,
"datetime": "2024-09-19T13:37:00+02:00",
"source": "pretixPOS",
"type": "badge",
"info": {
"cashier": 1234
}
}
:param organizer: The ``slug`` field of the organizer to create a log for
:param event: The ``slug`` field of the event to create a log for
:param id: The ``id`` field of the order position to create a log for
:statuscode 201: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource
**or** downloads are not available for this order position at this time. The response content will
contain more details.
:statuscode 404: The requested order position or download provider does not exist.
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
seconds.
Changing order contents Changing order contents
----------------------- -----------------------
+2 -2
View File
@@ -313,7 +313,7 @@ Endpoints for event exports
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource. :statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.
Endpoints for organizer exports Endpoints for organizer exports
------------------------------- ---------------------------
.. http:get:: /api/v1/organizers/(organizer)/scheduled_exports/ .. http:get:: /api/v1/organizers/(organizer)/scheduled_exports/
@@ -553,4 +553,4 @@ Endpoints for organizer exports
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource. :statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource.
.. _RFC 5545: https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.3 .. _RFC 5545: https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.3
+1 -1
View File
@@ -1,4 +1,4 @@
.. _`rest-seats`: .. _`rest-reusablemedia`:
Seats Seats
===== =====
-1
View File
@@ -17,7 +17,6 @@ First, you need to declare that you are using non-essential cookies by respondin
signal: signal:
.. automodule:: pretix.presale.signals .. automodule:: pretix.presale.signals
:no-index:
:members: register_cookie_providers :members: register_cookie_providers
You are expected to return a list of ``CookieProvider`` objects instantiated from the following class: You are expected to return a list of ``CookieProvider`` objects instantiated from the following class:
-10
View File
@@ -22,14 +22,12 @@ Order events
There are multiple signals that will be sent out in the ordering cycle: There are multiple signals that will be sent out in the ordering cycle:
.. automodule:: pretix.base.signals .. automodule:: pretix.base.signals
:no-index:
:members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text :members: validate_cart, validate_cart_addons, validate_order, order_valid_if_pending, order_fee_calculation, order_paid, order_placed, order_canceled, order_reactivated, order_expired, order_modified, order_changed, order_approved, order_denied, order_fee_type_name, allow_ticket_download, order_split, order_gracefully_delete, invoice_line_text
Check-ins Check-ins
""""""""" """""""""
.. automodule:: pretix.base.signals .. automodule:: pretix.base.signals
:no-index:
:members: checkin_created :members: checkin_created
@@ -41,21 +39,18 @@ Frontend
.. automodule:: pretix.presale.signals .. automodule:: pretix.presale.signals
:no-index:
:members: order_info, order_info_top, order_meta_from_request, order_api_meta_from_request :members: order_info, order_info_top, order_meta_from_request, order_api_meta_from_request
Request flow Request flow
"""""""""""" """"""""""""
.. automodule:: pretix.presale.signals .. automodule:: pretix.presale.signals
:no-index:
:members: process_request, process_response :members: process_request, process_response
Vouchers Vouchers
"""""""" """"""""
.. automodule:: pretix.presale.signals .. automodule:: pretix.presale.signals
:no-index:
:members: voucher_redeem_info :members: voucher_redeem_info
Backend Backend
@@ -67,28 +62,24 @@ Backend
item_formsets, order_search_filter_q, order_search_forms item_formsets, order_search_filter_q, order_search_forms
.. automodule:: pretix.base.signals .. automodule:: pretix.base.signals
:no-index:
:members: logentry_display, logentry_object_link, requiredaction_display, timeline_events, orderposition_blocked_display, customer_created, customer_signed_in :members: logentry_display, logentry_object_link, requiredaction_display, timeline_events, orderposition_blocked_display, customer_created, customer_signed_in
Vouchers Vouchers
"""""""" """"""""
.. automodule:: pretix.control.signals .. automodule:: pretix.control.signals
:no-index:
:members: item_forms, voucher_form_class, voucher_form_html, voucher_form_validation :members: item_forms, voucher_form_class, voucher_form_html, voucher_form_validation
Dashboards Dashboards
"""""""""" """"""""""
.. automodule:: pretix.control.signals .. automodule:: pretix.control.signals
:no-index:
:members: event_dashboard_widgets, user_dashboard_widgets, event_dashboard_top :members: event_dashboard_widgets, user_dashboard_widgets, event_dashboard_top
Ticket designs Ticket designs
"""""""""""""" """"""""""""""
.. automodule:: pretix.base.signals .. automodule:: pretix.base.signals
:no-index:
:members: layout_text_variables, layout_image_variables :members: layout_text_variables, layout_image_variables
.. automodule:: pretix.plugins.ticketoutputpdf.signals .. automodule:: pretix.plugins.ticketoutputpdf.signals
@@ -98,5 +89,4 @@ API
--- ---
.. automodule:: pretix.base.signals .. automodule:: pretix.base.signals
:no-index:
:members: validate_event_settings, api_event_settings_fields :members: validate_event_settings, api_event_settings_fields
-1
View File
@@ -60,7 +60,6 @@ that we'll provide in this plugin:
Similar signals exist for other objects: Similar signals exist for other objects:
.. automodule:: pretix.base.signals .. automodule:: pretix.base.signals
:no-index:
:members: voucher_import_columns :members: voucher_import_columns
+2
View File
@@ -84,6 +84,8 @@ convenient to you:
.. automethod:: _register_fonts .. automethod:: _register_fonts
.. automethod:: _register_event_fonts
.. automethod:: _on_first_page .. automethod:: _on_first_page
.. automethod:: _on_other_page .. automethod:: _on_other_page
-3
View File
@@ -86,10 +86,7 @@ Signals
------- -------
.. automodule:: pretix.base.signals .. automodule:: pretix.base.signals
:no-index:
:members: register_text_placeholders :members: register_text_placeholders
.. automodule:: pretix.base.signals .. automodule:: pretix.base.signals
:no-index:
:members: register_mail_placeholders :members: register_mail_placeholders
+1 -1
View File
@@ -1,5 +1,5 @@
KulturPass KulturPass
========== =========
.. note:: .. note::
+1 -1
View File
@@ -158,7 +158,7 @@ expects and - more importantly - supports.
for a sample configuration in an academic context. for a sample configuration in an academic context.
Note, that you can have multiple attributes with the same ``friendlyName`` Note, that you can have multiple attributes with the same ``friendlyName``
but different ``name`` value. This is often used in systems, where the same but different ``name``s. This is often used in systems, where the same
information (for example a persons name) is saved in different fields - information (for example a persons name) is saved in different fields -
for example because one institution is returning SAML 1.0 and other for example because one institution is returning SAML 1.0 and other
institutions are returning SAML 2.0 style attributes. Typically, this only institutions are returning SAML 2.0 style attributes. Typically, this only
+6 -5
View File
@@ -43,7 +43,7 @@ dependencies = [
"django-formset-js-improved==0.5.0.3", "django-formset-js-improved==0.5.0.3",
"django-formtools==2.5.1", "django-formtools==2.5.1",
"django-hierarkey==1.2.*", "django-hierarkey==1.2.*",
"django-hijack==3.7.*", "django-hijack==3.6.*",
"django-i18nfield==1.9.*,>=1.9.4", "django-i18nfield==1.9.*,>=1.9.4",
"django-libsass==0.9", "django-libsass==0.9",
"django-localflavor==4.0", "django-localflavor==4.0",
@@ -74,21 +74,21 @@ dependencies = [
"paypal-checkout-serversdk==1.0.*", "paypal-checkout-serversdk==1.0.*",
"PyJWT==2.9.*", "PyJWT==2.9.*",
"phonenumberslite==8.13.*", "phonenumberslite==8.13.*",
"Pillow==11.0.*", "Pillow==10.4.*",
"pretix-plugin-build", "pretix-plugin-build",
"protobuf==5.28.*", "protobuf==5.28.*",
"psycopg2-binary", "psycopg2-binary",
"pycountry", "pycountry",
"pycparser==2.22", "pycparser==2.22",
"pycryptodome==3.21.*", "pycryptodome==3.21.*",
"pypdf==5.1.*", "pypdf==5.0.*",
"python-bidi==0.6.*", # Support for Arabic in reportlab "python-bidi==0.6.*", # Support for Arabic in reportlab
"python-dateutil==2.9.*", "python-dateutil==2.9.*",
"pytz", "pytz",
"pytz-deprecation-shim==0.1.*", "pytz-deprecation-shim==0.1.*",
"pyuca", "pyuca",
"qrcode==8.0", "qrcode==8.0",
"redis==5.2.*", "redis==5.1.*",
"reportlab==4.2.*", "reportlab==4.2.*",
"requests==2.31.*", "requests==2.31.*",
"sentry-sdk==2.17.*", "sentry-sdk==2.17.*",
@@ -102,7 +102,7 @@ dependencies = [
"vat_moss_forked==2020.3.20.0.11.0", "vat_moss_forked==2020.3.20.0.11.0",
"vobject==0.9.*", "vobject==0.9.*",
"webauthn==2.2.*", "webauthn==2.2.*",
"zeep==4.3.*" "zeep==4.2.*"
] ]
[project.optional-dependencies] [project.optional-dependencies]
@@ -122,6 +122,7 @@ dev = [
"pytest-cov", "pytest-cov",
"pytest-django==4.*", "pytest-django==4.*",
"pytest-mock==3.14.*", "pytest-mock==3.14.*",
"pytest-rerunfailures==14.*",
"pytest-sugar", "pytest-sugar",
"pytest-xdist==3.6.*", "pytest-xdist==3.6.*",
"pytest==8.3.*", "pytest==8.3.*",
+1 -1
View File
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
__version__ = "2024.11.0.dev0" __version__ = "2024.10.0.dev0"
-4
View File
@@ -77,7 +77,6 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:blockedsecrets-list'), ('GET', 'api-v1:blockedsecrets-list'),
('GET', 'api-v1:order-list'), ('GET', 'api-v1:order-list'),
('GET', 'api-v1:orderposition-pdf_image'), ('GET', 'api-v1:orderposition-pdf_image'),
('POST', 'api-v1:orderposition-printlog'),
('GET', 'api-v1:event.settings'), ('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'), ('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'), ('POST', 'api-v1:checkinrpc.redeem'),
@@ -113,7 +112,6 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:revokedsecrets-list'), ('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:blockedsecrets-list'), ('GET', 'api-v1:blockedsecrets-list'),
('GET', 'api-v1:orderposition-pdf_image'), ('GET', 'api-v1:orderposition-pdf_image'),
('POST', 'api-v1:orderposition-printlog'),
('GET', 'api-v1:event.settings'), ('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'), ('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'), ('POST', 'api-v1:checkinrpc.redeem'),
@@ -149,7 +147,6 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:revokedsecrets-list'), ('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:blockedsecrets-list'), ('GET', 'api-v1:blockedsecrets-list'),
('GET', 'api-v1:orderposition-pdf_image'), ('GET', 'api-v1:orderposition-pdf_image'),
('POST', 'api-v1:orderposition-printlog'),
('GET', 'api-v1:event.settings'), ('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'), ('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'), ('POST', 'api-v1:checkinrpc.redeem'),
@@ -191,7 +188,6 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:orderposition-list'), ('GET', 'api-v1:orderposition-list'),
('GET', 'api-v1:orderposition-answer'), ('GET', 'api-v1:orderposition-answer'),
('GET', 'api-v1:orderposition-pdf_image'), ('GET', 'api-v1:orderposition-pdf_image'),
('POST', 'api-v1:orderposition-printlog'),
('POST', 'api-v1:order-mark-canceled'), ('POST', 'api-v1:order-mark-canceled'),
('POST', 'api-v1:orderpayment-list'), ('POST', 'api-v1:orderpayment-list'),
('POST', 'api-v1:orderrefund-list'), ('POST', 'api-v1:orderrefund-list'),
+5 -27
View File
@@ -55,7 +55,7 @@ from pretix.base.models import (
) )
from pretix.base.models.orders import ( from pretix.base.models.orders import (
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund, BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
PrintLog, RevokedTicketSecret, RevokedTicketSecret,
) )
from pretix.base.pdf import get_images, get_variables from pretix.base.pdf import get_images, get_variables
from pretix.base.services.cart import error_messages from pretix.base.services.cart import error_messages
@@ -284,26 +284,6 @@ class CheckinSerializer(I18nAwareModelSerializer):
fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'device_id', 'type') fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'device_id', 'type')
class PrintLogSerializer(serializers.ModelSerializer):
device_id = serializers.SlugRelatedField(
source='device',
slug_field='device_id',
read_only=True,
)
class Meta:
model = PrintLog
fields = (
"id",
"successful",
"datetime",
"source",
"type",
"device_id",
"info",
)
class FailedCheckinSerializer(I18nAwareModelSerializer): class FailedCheckinSerializer(I18nAwareModelSerializer):
error_reason = serializers.ChoiceField(choices=Checkin.REASONS, required=True, allow_null=False) error_reason = serializers.ChoiceField(choices=Checkin.REASONS, required=True, allow_null=False)
raw_barcode = serializers.CharField(required=True, allow_null=False) raw_barcode = serializers.CharField(required=True, allow_null=False)
@@ -496,7 +476,6 @@ class OrderPositionListSerializer(serializers.ListSerializer):
class OrderPositionSerializer(I18nAwareModelSerializer): class OrderPositionSerializer(I18nAwareModelSerializer):
checkins = CheckinSerializer(many=True, read_only=True) checkins = CheckinSerializer(many=True, read_only=True)
print_logs = PrintLogSerializer(many=True, read_only=True)
answers = AnswerSerializer(many=True) answers = AnswerSerializer(many=True)
downloads = PositionDownloadsField(source='*', read_only=True) downloads = PositionDownloadsField(source='*', read_only=True)
order = serializers.SlugRelatedField(slug_field='code', read_only=True) order = serializers.SlugRelatedField(slug_field='code', read_only=True)
@@ -511,7 +490,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount', 'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
'valid_from', 'valid_until', 'blocked', 'voucher_budget_use') 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use')
read_only_fields = ( read_only_fields = (
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret', 'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
@@ -598,9 +577,9 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts',
'company', 'street', 'zipcode', 'city', 'country', 'state', 'company', 'street', 'zipcode', 'city', 'country', 'state',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'require_attention',
'require_attention', 'order__status', 'order__valid_if_pending', 'order__require_approval', 'order__status', 'order__valid_if_pending', 'order__require_approval', 'valid_from', 'valid_until',
'valid_from', 'valid_until', 'blocked') 'blocked')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -1515,7 +1494,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
pos.answers = answers pos.answers = answers
pos.pseudonymization_id = "PREVIEW" pos.pseudonymization_id = "PREVIEW"
pos.checkins = [] pos.checkins = []
pos.print_logs = []
pos_map[pos.positionid] = pos pos_map[pos.positionid] = pos
else: else:
if pos.voucher: if pos.voucher:
+2 -6
View File
@@ -62,7 +62,6 @@ from pretix.base.models import (
CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition, CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition,
Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken, Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken,
) )
from pretix.base.models.orders import PrintLog
from pretix.base.services.checkin import ( from pretix.base.services.checkin import (
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin, CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
) )
@@ -366,9 +365,8 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
qs = qs.prefetch_related( qs = qs.prefetch_related(
Prefetch( Prefetch(
lookup='checkins', lookup='checkins',
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device') queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists])
), ),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question', 'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')), Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related( Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
@@ -380,7 +378,6 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
'positions', 'positions',
OrderPosition.objects.prefetch_related( OrderPosition.objects.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related('device')), Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'item', 'variation', 'answers', 'answers__options', 'answers__question', 'item', 'variation', 'answers', 'answers__options', 'answers__question',
) )
) )
@@ -392,9 +389,8 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
qs = qs.prefetch_related( qs = qs.prefetch_related(
Prefetch( Prefetch(
lookup='checkins', lookup='checkins',
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device') queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists])
), ),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question', 'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')) Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat') ).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
+2 -13
View File
@@ -20,7 +20,6 @@
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
import base64 import base64
import copy
import logging import logging
from cryptography.hazmat.backends.openssl.backend import Backend from cryptography.hazmat.backends.openssl.backend import Backend
@@ -147,8 +146,6 @@ class InitializeView(APIView):
permission_classes = () permission_classes = ()
def post(self, request, format=None): def post(self, request, format=None):
from pretix.base.signals import device_info_updated
serializer = InitializationRequestSerializer(data=request.data) serializer = InitializationRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@@ -163,8 +160,6 @@ class InitializeView(APIView):
if device.revoked: if device.revoked:
raise ValidationError({'token': ['This initialization token has been revoked.']}) raise ValidationError({'token': ['This initialization token has been revoked.']})
old_instance = copy.copy(device)
device.initialized = now() device.initialized = now()
device.hardware_brand = serializer.validated_data.get('hardware_brand') device.hardware_brand = serializer.validated_data.get('hardware_brand')
device.hardware_model = serializer.validated_data.get('hardware_model') device.hardware_model = serializer.validated_data.get('hardware_model')
@@ -179,10 +174,6 @@ class InitializeView(APIView):
device.log_action('pretix.device.initialized', data=serializer.validated_data, auth=device) device.log_action('pretix.device.initialized', data=serializer.validated_data, auth=device)
device_info_updated.send(
sender=Device, old_device=old_instance, new_device=device
)
serializer = DeviceSerializer(device) serializer = DeviceSerializer(device)
return Response(serializer.data) return Response(serializer.data)
@@ -191,12 +182,9 @@ class UpdateView(APIView):
authentication_classes = (DeviceTokenAuthentication,) authentication_classes = (DeviceTokenAuthentication,)
def post(self, request, format=None): def post(self, request, format=None):
from pretix.base.signals import device_info_updated
serializer = UpdateRequestSerializer(data=request.data) serializer = UpdateRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
device = request.auth device = request.auth
old_instance = copy.copy(device)
device.hardware_brand = serializer.validated_data.get('hardware_brand') device.hardware_brand = serializer.validated_data.get('hardware_brand')
device.hardware_model = serializer.validated_data.get('hardware_model') device.hardware_model = serializer.validated_data.get('hardware_model')
device.os_name = serializer.validated_data.get('os_name') device.os_name = serializer.validated_data.get('os_name')
@@ -212,8 +200,9 @@ class UpdateView(APIView):
device.save() device.save()
device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device) device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device)
from ...base.signals import device_info_updated
device_info_updated.send( device_info_updated.send(
sender=Device, old_device=old_instance, new_device=device sender=Device, old_device=request.auth, new_device=device
) )
serializer = DeviceSerializer(device) serializer = DeviceSerializer(device)
-2
View File
@@ -42,7 +42,6 @@ from pretix.base.models import (
Checkin, GiftCard, GiftCardAcceptance, GiftCardTransaction, OrderPosition, Checkin, GiftCard, GiftCardAcceptance, GiftCardTransaction, OrderPosition,
ReusableMedium, ReusableMedium,
) )
from pretix.base.models.orders import PrintLog
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
from pretix.helpers.dicts import merge_dicts from pretix.helpers.dicts import merge_dicts
@@ -80,7 +79,6 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
'order', 'order__event', 'order__event__organizer', 'seat', 'order', 'order__event', 'order__event__organizer', 'seat',
).prefetch_related( ).prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related('device')), Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question', 'answers', 'answers__options', 'answers__question',
) )
), ),
+2 -35
View File
@@ -57,8 +57,7 @@ from pretix.api.serializers.order import (
OrderPaymentCreateSerializer, OrderPaymentSerializer, OrderPaymentCreateSerializer, OrderPaymentSerializer,
OrderPositionSerializer, OrderRefundCreateSerializer, OrderPositionSerializer, OrderRefundCreateSerializer,
OrderRefundSerializer, OrderSerializer, PriceCalcSerializer, OrderRefundSerializer, OrderSerializer, PriceCalcSerializer,
PrintLogSerializer, RevokedTicketSecretSerializer, RevokedTicketSecretSerializer, SimulatedOrderSerializer,
SimulatedOrderSerializer,
) )
from pretix.api.serializers.orderchange import ( from pretix.api.serializers.orderchange import (
BlockNameSerializer, OrderChangeOperationSerializer, BlockNameSerializer, OrderChangeOperationSerializer,
@@ -76,7 +75,7 @@ from pretix.base.models import (
TeamAPIToken, generate_secret, TeamAPIToken, generate_secret,
) )
from pretix.base.models.orders import ( from pretix.base.models.orders import (
BlockedTicketSecret, PrintLog, QuestionAnswer, RevokedTicketSecret, BlockedTicketSecret, QuestionAnswer, RevokedTicketSecret,
) )
from pretix.base.payment import PaymentException from pretix.base.payment import PaymentException
from pretix.base.pdf import get_images from pretix.base.pdf import get_images
@@ -260,7 +259,6 @@ class OrderViewSetMixin:
'positions', 'positions',
opq.all().prefetch_related( opq.all().prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related('device')), Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
Prefetch('item', queryset=self.request.event.items.prefetch_related( Prefetch('item', queryset=self.request.event.items.prefetch_related(
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached') Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached')
)), )),
@@ -282,7 +280,6 @@ class OrderViewSetMixin:
'positions', 'positions',
opq.all().prefetch_related( opq.all().prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related('device')), Prefetch('checkins', queryset=Checkin.objects.select_related('device')),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'item', 'variation', 'item', 'variation',
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')), Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')),
'seat', 'seat',
@@ -1096,7 +1093,6 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
) )
qs = qs.prefetch_related( qs = qs.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related("device")), Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
Prefetch('item', queryset=self.request.event.items.prefetch_related( Prefetch('item', queryset=self.request.event.items.prefetch_related(
Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), Prefetch('meta_values', ItemMetaValue.objects.select_related('property'),
to_attr='meta_values_cached') to_attr='meta_values_cached')
@@ -1140,7 +1136,6 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
else: else:
qs = qs.prefetch_related( qs = qs.prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.select_related("device")), Prefetch('checkins', queryset=Checkin.objects.select_related("device")),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question', 'answers', 'answers__options', 'answers__question',
).select_related( ).select_related(
'item', 'order', 'order__event', 'order__event__organizer', 'seat' 'item', 'order', 'order__event', 'order__event__organizer', 'seat'
@@ -1259,34 +1254,6 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
) )
return resp return resp
@action(detail=True, url_name="printlog", url_path="printlog", methods=["POST"])
def printlog(self, request, **kwargs):
pos = self.get_object()
serializer = PrintLogSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
with transaction.atomic():
serializer.save(
position=pos,
device=request.auth if isinstance(request.auth, Device) else None,
user=request.user if request.user.is_authenticated else None,
api_token=request.auth if isinstance(request.auth, TeamAPIToken) else None,
oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None,
)
pos.order.log_action(
"pretix.event.order.print",
data={
"position": pos.pk,
"positionid": pos.positionid,
**serializer.validated_data,
},
auth=request.auth,
user=request.user,
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@action(detail=True, url_name='pdf_image', url_path=r'pdf_image/(?P<key>[^/]+)') @action(detail=True, url_name='pdf_image', url_path=r'pdf_image/(?P<key>[^/]+)')
def pdf_image(self, request, key, **kwargs): def pdf_image(self, request, key, **kwargs):
pos = self.get_object() pos = self.get_object()
@@ -1,79 +0,0 @@
# Generated by Django 4.2.16 on 2024-09-19 10:41
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
("pretixbase", "0271_itemcategory_cross_selling"),
]
operations = [
migrations.CreateModel(
name="PrintLog",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("datetime", models.DateTimeField(default=django.utils.timezone.now)),
("created", models.DateTimeField(auto_now_add=True, null=True)),
("successful", models.BooleanField(default=True)),
("source", models.CharField(max_length=255)),
("type", models.CharField(max_length=255)),
("info", models.JSONField(default=dict)),
(
"api_token",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="pretixbase.teamapitoken",
),
),
(
"device",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="print_logs",
to="pretixbase.device",
),
),
(
"oauth_application",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL,
),
),
(
"position",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="print_logs",
to="pretixbase.orderposition",
),
),
(
"user",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="print_logs",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ("-datetime",),
},
),
]
+2 -1
View File
@@ -1118,12 +1118,13 @@ class ItemVariation(models.Model):
:param original_price: The item's "original" price. Will not be used for any calculations, will just be shown. :param original_price: The item's "original" price. Will not be used for any calculations, will just be shown.
:type original_price: decimal.Decimal :type original_price: decimal.Decimal
:param require_approval: If set to ``True``, orders containing this variation can only be processed and paid after :param require_approval: If set to ``True``, orders containing this variation can only be processed and paid after
approval by an administrator approval by an administrator
:type require_approval: bool :type require_approval: bool
:param all_sales_channels: A flag indicating that this variation is available on all channels and limit_sales_channels will be ignored. :param all_sales_channels: A flag indicating that this variation is available on all channels and limit_sales_channels will be ignored.
:type all_sales_channels: bool :type all_sales_channels: bool
:param limit_sales_channels: A list of sales channel identifiers, that this variation is available for sale on. :param limit_sales_channels: A list of sales channel identifiers, that this variation is available for sale on.
:type limit_sales_channels: list :type limit_sales_channels: list
""" """
item = models.ForeignKey( item = models.ForeignKey(
Item, Item,
-68
View File
@@ -3391,74 +3391,6 @@ class BlockedTicketSecret(models.Model):
unique_together = (('event', 'secret'),) unique_together = (('event', 'secret'),)
class PrintLog(models.Model):
"""
A print log object is created when a ticket or badge is printed with our apps.
"""
TYPE_BADGE = 'badge'
TYPE_TICKET = 'ticket'
TYPE_CERTIFICATE = 'certificate'
TYPE_OTHER = 'other'
PRINT_TYPES = (
(TYPE_BADGE, _('Badge')),
(TYPE_TICKET, _('Ticket')),
(TYPE_CERTIFICATE, _('Certificate')),
(TYPE_OTHER, _('Other')),
)
position = models.ForeignKey(
'pretixbase.OrderPosition',
related_name='print_logs',
on_delete=models.CASCADE,
)
successful = models.BooleanField(
default=True,
)
# Datetime of checkin, might be different from created if past scans are uploaded
datetime = models.DateTimeField(default=now)
# Datetime of creation on server
created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
# Who printed?
device = models.ForeignKey('Device', related_name='print_logs', null=True, blank=True, on_delete=models.PROTECT)
user = models.ForeignKey('User', related_name='print_logs', null=True, blank=True, on_delete=models.PROTECT)
api_token = models.ForeignKey('TeamAPIToken', null=True, blank=True, on_delete=models.PROTECT)
oauth_application = models.ForeignKey('pretixapi.OAuthApplication', null=True, blank=True, on_delete=models.PROTECT)
# Source = Tag field with undefined values, e.g. name of app ("pretixscan")
source = models.CharField(max_length=255)
# Type = Type of object printed ("badge", "ticket")
type = models.CharField(max_length=255, choices=PRINT_TYPES)
info = models.JSONField(default=dict)
objects = ScopedManager(organizer='position__order__event__organizer')
class Meta:
ordering = (('-datetime'),)
def __repr__(self):
return "<PrintLog: pos {} at {} from {}>".format(
self.position, self.datetime, self.source
)
def save(self, **kwargs):
super().save(**kwargs)
if self.position:
self.position.order.touch()
def delete(self, **kwargs):
super().delete(**kwargs)
self.position.order.touch()
@property
def is_late_upload(self):
return self.created and abs(self.created - self.datetime) > timedelta(minutes=2)
@receiver(post_delete, sender=CachedTicket) @receiver(post_delete, sender=CachedTicket)
def cachedticket_delete(sender, instance, **kwargs): def cachedticket_delete(sender, instance, **kwargs):
if instance.file: if instance.file:
-24
View File
@@ -53,30 +53,6 @@ class SeatingPlanLayoutValidator:
e = str(e).replace('%', '%%') e = str(e).replace('%', '%%')
raise ValidationError(_('Your layout file is not a valid seating plan. Error message: {}').format(e)) raise ValidationError(_('Your layout file is not a valid seating plan. Error message: {}').format(e))
try:
seat_guids = set()
for z in val["zones"]:
for r in z["rows"]:
for s in r["seats"]:
if not s.get("seat_guid"):
raise ValidationError(
_("Seat with zone {zone}, row {row}, and number {number} has no seat ID.").format(
zone=z["name"],
row=r["row_number"],
number=s["seat_number"],
)
)
elif s["seat_guid"] in seat_guids:
raise ValidationError(
_("Multiple seats have the same ID: {id}").format(
id=s["seat_guid"],
)
)
seat_guids.add(s["seat_guid"])
except ValidationError as e:
raise ValidationError(_('Your layout file is not a valid seating plan. Error message: {}').format(", ".join(e.message for e in e.error_list)))
class SeatingPlan(LoggedModel): class SeatingPlan(LoggedModel):
""" """
+1 -1
View File
@@ -367,7 +367,7 @@ validate_cart_addons = EventPluginSignal()
Arguments: ``addons``, ``base_position``, ``iao`` Arguments: ``addons``, ``base_position``, ``iao``
This signal is sent when a user tries to select a combination of addons. In contrast to This signal is sent when a user tries to select a combination of addons. In contrast to
``validate_cart``, this is executed before the cart is actually modified. You are passed ``validate_cart``, this is executed before the cart is actually modified. You are passed
an argument ``addons`` containing a dict of ``(item, variation or None) → count`` tuples as well an argument ``addons`` containing a dict of ``(item, variation or None) → count`` tuples as well
as the ``ItemAddOn`` object as the argument ``iao`` and the base cart position as as the ``ItemAddOn`` object as the argument ``iao`` and the base cart position as
``base_position``. ``base_position``.
-11
View File
@@ -51,7 +51,6 @@ from pretix.base.models import (
Checkin, CheckinList, Event, ItemVariation, LogEntry, OrderPosition, Checkin, CheckinList, Event, ItemVariation, LogEntry, OrderPosition,
TaxRule, TaxRule,
) )
from pretix.base.models.orders import PrintLog
from pretix.base.signals import logentry_display, orderposition_blocked_display from pretix.base.signals import logentry_display, orderposition_blocked_display
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
@@ -640,16 +639,6 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
if sender and logentry.action_type.startswith('pretix.event.checkin'): if sender and logentry.action_type.startswith('pretix.event.checkin'):
return _display_checkin(sender, logentry) return _display_checkin(sender, logentry)
if logentry.action_type == 'pretix.event.order.print':
return _('Position #{posid} has been printed at {datetime} with type "{type}".').format(
posid=data.get('positionid'),
datetime=date_format(
dateutil.parser.parse(data["datetime"]).astimezone(sender.timezone),
"SHORT_DATETIME_FORMAT"
),
type=dict(PrintLog.PRINT_TYPES)[data["type"]],
)
if logentry.action_type == 'pretix.control.views.checkin': if logentry.action_type == 'pretix.control.views.checkin':
# deprecated # deprecated
dt = dateutil.parser.parse(data.get('datetime')) dt = dateutil.parser.parse(data.get('datetime'))
@@ -12,22 +12,10 @@
{% endif %} {% endif %}
{% if object.id and not object.quotas.exists %} {% if object.id and not object.quotas.exists %}
<div class="alert alert-warning"> <div class="alert alert-warning">
<div class="row"> {% blocktrans trimmed %}
<div class="col-lg-8"> Please note that your product will <strong>not</strong> be available for sale until you have added your
{% blocktrans trimmed %} item to an existing or newly created quota.
Please note that your product will <strong>not</strong> be available for sale until you have added your {% endblocktrans %}
item to an existing or newly created quota.
{% endblocktrans %}
</div>
<div class="col-lg-4 text-right">
<a class="btn btn-default btn-sm" href="{% url "control:event.items.quotas" organizer=request.event.organizer.slug event=request.event.slug %}">
<i class="fa fa-wrench"></i> {% trans "Manage quotas" %}
</a>
<a class="btn btn-default btn-sm" href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}?{% if object.has_variations %}{% for var in object.variations.all %}product={{ object.pk }}-{{ var.pk }}&{% endfor %}{% else %}product={{ object.pk }}{% endif %}">
<i class="fa fa-plus"></i> {% trans "Create a new quota" %}
</a>
</div>
</div>
</div> </div>
{% elif object.pk and not object.is_available_by_time %} {% elif object.pk and not object.is_available_by_time %}
<div class="alert alert-warning"> <div class="alert alert-warning">
@@ -67,22 +67,10 @@
<div class="panel-body form-horizontal"> <div class="panel-body form-horizontal">
{% if form.instance.pk and not form.instance.quotas.exists %} {% if form.instance.pk and not form.instance.quotas.exists %}
<div class="alert alert-warning"> <div class="alert alert-warning">
<div class="row"> {% blocktrans trimmed %}
<div class="col-lg-8"> Please note that your variation will <strong>not</strong> be available for sale
{% blocktrans trimmed %} until you have added it to an existing or newly created quota.
Please note that your variation will <strong>not</strong> be available for sale {% endblocktrans %}
until you have added it to an existing or newly created quota.
{% endblocktrans %}
</div>
<div class="col-lg-4 text-right">
<a class="btn btn-default btn-xs" href="{% url "control:event.items.quotas" organizer=request.event.organizer.slug event=request.event.slug %}">
<i class="fa fa-wrench"></i> {% trans "Manage quotas" %}
</a>
<a class="btn btn-default btn-xs" href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}?product={{ form.instance.item.pk }}-{{ form.instance.pk }}">
<i class="fa fa-plus"></i> {% trans "Create a new quota" %}
</a>
</div>
</div>
</div> </div>
{% endif %} {% endif %}
{% bootstrap_form_errors form %} {% bootstrap_form_errors form %}
+1 -4
View File
@@ -920,19 +920,16 @@ class QuotaCreate(EventPermissionRequiredMixin, CreateView):
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs.setdefault('initial', {})
if self.copy_from: if self.copy_from:
i = modelcopy(self.copy_from) i = modelcopy(self.copy_from)
i.pk = None i.pk = None
kwargs['instance'] = i kwargs['instance'] = i
kwargs.setdefault('initial', {})
kwargs['initial']['itemvars'] = [str(i.pk) for i in self.copy_from.items.all()] + [ kwargs['initial']['itemvars'] = [str(i.pk) for i in self.copy_from.items.all()] + [
'{}-{}'.format(v.item_id, v.pk) for v in self.copy_from.variations.all() '{}-{}'.format(v.item_id, v.pk) for v in self.copy_from.variations.all()
] ]
else: else:
kwargs['instance'] = Quota(event=self.request.event) kwargs['instance'] = Quota(event=self.request.event)
if 'product' in self.request.GET:
kwargs['initial']['itemvars'] = self.request.GET.getlist('product')
return kwargs return kwargs
def form_invalid(self, form): def form_invalid(self, form):
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-10-29 08:54+0000\n" "POT-Creation-Date: 2024-10-15 11:14+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -8
View File
@@ -25,19 +25,12 @@ from rest_framework import serializers, viewsets
from pretix.api.pagination import TotalOrderingFilter from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.i18n import I18nAwareModelSerializer from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import ItemVariation, SalesChannel from pretix.base.models import ItemVariation
from pretix.plugins.autocheckin.models import AutoCheckinRule from pretix.plugins.autocheckin.models import AutoCheckinRule
from pretix.plugins.sendmail.models import Rule from pretix.plugins.sendmail.models import Rule
class AutoCheckinRuleSerializer(I18nAwareModelSerializer): class AutoCheckinRuleSerializer(I18nAwareModelSerializer):
limit_sales_channels = serializers.SlugRelatedField(
slug_field="identifier",
queryset=SalesChannel.objects.none(),
required=False,
allow_empty=True,
many=True,
)
class Meta: class Meta:
model = AutoCheckinRule model = AutoCheckinRule
+2 -37
View File
@@ -462,28 +462,14 @@ class BadgeExporter(BaseExporter):
)), )),
('date_from', ('date_from',
forms.DateField( forms.DateField(
label=_('Start event date'), label=_('Start date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}), widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False, required=False,
help_text=_('Only include tickets for dates on or after this date.') help_text=_('Only include tickets for dates on or after this date.')
)), )),
('date_to', ('date_to',
forms.DateField( forms.DateField(
label=_('End event date'), label=_('End date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=_('Only include tickets ordered on or before this date.')
)),
('order_date_from',
forms.DateField(
label=_('Start order date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=_('Only include tickets ordered on or after this date.')
)),
('order_date_to',
forms.DateField(
label=_('End order date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}), widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False, required=False,
help_text=_('Only include tickets for dates on or before this date.') help_text=_('Only include tickets for dates on or before this date.')
@@ -495,7 +481,6 @@ class BadgeExporter(BaseExporter):
('name', _('Attendee name')), ('name', _('Attendee name')),
('company', _('Attendee company')), ('company', _('Attendee company')),
('code', _('Order code')), ('code', _('Order code')),
('order_date', _('Order date')),
('date', _('Event date')), ('date', _('Event date')),
] + ([ ] + ([
('name:{}'.format(k), _('Attendee name: {part}').format(part=label)) ('name:{}'.format(k), _('Attendee name: {part}').format(part=label))
@@ -548,24 +533,6 @@ class BadgeExporter(BaseExporter):
), self.event.timezone) ), self.event.timezone)
qs = qs.filter(Q(subevent__date_from__lt=dt) | Q(subevent__isnull=True, order__event__date_from__lt=dt)) qs = qs.filter(Q(subevent__date_from__lt=dt) | Q(subevent__isnull=True, order__event__date_from__lt=dt))
if form_data.get('order_date_from'):
if not isinstance(form_data.get('order_date_from'), date):
form_data['order_date_from'] = dateutil.parser.parse(form_data['order_date_from']).date()
df = make_aware(datetime.combine(
form_data['order_date_from'],
time(hour=0, minute=0, second=0)
), self.event.timezone)
qs = qs.filter(order__datetime__gte=df)
if form_data.get('order_date_to'):
if not isinstance(form_data.get('order_date_to'), date):
form_data['order_date_to'] = dateutil.parser.parse(form_data['order_date_to']).date()
dt = make_aware(datetime.combine(
form_data['order_date_to'] + timedelta(days=1),
time(hour=0, minute=0, second=0)
), self.event.timezone)
qs = qs.filter(order__datetime__lt=dt)
if form_data.get('order_by') == 'name': if form_data.get('order_by') == 'name':
qs = qs.annotate( qs = qs.annotate(
resolved_name=Case( resolved_name=Case(
@@ -586,8 +553,6 @@ class BadgeExporter(BaseExporter):
).order_by('resolved_company', 'order__code') ).order_by('resolved_company', 'order__code')
elif form_data.get('order_by') == 'code': elif form_data.get('order_by') == 'code':
qs = qs.order_by('order__code') qs = qs.order_by('order__code')
elif form_data.get('order_by') == 'order_date':
qs = qs.order_by('order__datetime')
elif form_data.get('order_by') == 'date': elif form_data.get('order_by') == 'date':
qs = qs.annotate(ed=Coalesce('subevent__date_from', 'order__event__date_from')).order_by('ed', 'order__code') qs = qs.annotate(ed=Coalesce('subevent__date_from', 'order__event__date_from')).order_by('ed', 'order__code')
elif form_data.get('order_by', '').startswith('name:'): elif form_data.get('order_by', '').startswith('name:'):
@@ -57,29 +57,16 @@
</div> </div>
{% if cross_selling_data %} {% if cross_selling_data %}
{% if forms %} <details class="panel panel-default cross-selling" open>
<details class="panel panel-default cross-selling" open> <summary class="panel-heading">
<summary class="panel-heading"> <h3 class="panel-title">
<h3 class="panel-title"> {% trans "More recommendations" %}
{% trans "More recommendations" %} </h3>
</h3> </summary>
</summary>
{% else %}
<div class="panel panel-default cross-selling">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Our recommendations" %}
</h3>
</div>
{% endif %}
<div class="panel-body"> <div class="panel-body">
{% include "pretixpresale/event/fragment_product_list.html" with items_by_category=cross_selling_data ev=event headline_level=4 %} {% include "pretixpresale/event/fragment_product_list.html" with items_by_category=cross_selling_data ev=event %}
</div> </div>
{% if forms %} </details>
</details>
{% else %}
</div>
{% endif %}
{% endif %} {% endif %}
<div class="row checkout-button-row"> <div class="row checkout-button-row">
@@ -8,7 +8,7 @@
{% for tup in items_by_category %}{% with category=tup.0 items=tup.1 form_prefix=tup.2 %} {% for tup in items_by_category %}{% with category=tup.0 items=tup.1 form_prefix=tup.2 %}
{% if category %} {% if category %}
<section aria-labelledby="{{ form_prefix }}category-{{ category.id }}"{% if category.description %} aria-describedby="{{ form_prefix }}category-info-{{ category.id }}"{% endif %}> <section aria-labelledby="{{ form_prefix }}category-{{ category.id }}"{% if category.description %} aria-describedby="{{ form_prefix }}category-info-{{ category.id }}"{% endif %}>
<h{{ headline_level|default:3 }} class="h3" id="{{ form_prefix }}category-{{ category.id }}">{{ category.name }} <h3 id="{{ form_prefix }}category-{{ category.id }}">{{ category.name }}
{% if category.subevent_name %} {% if category.subevent_name %}
<small class="text-muted"><i class="fa fa-calendar"></i> {{ category.subevent_name }}</small> <small class="text-muted"><i class="fa fa-calendar"></i> {{ category.subevent_name }}</small>
{% endif %} {% endif %}
@@ -19,13 +19,13 @@
{% trans "Your order qualifies for a discount" %} {% trans "Your order qualifies for a discount" %}
</small> </small>
{% endif %} {% endif %}
</h{{ headline_level|default:3 }}> </h3>
{% if category.description %} {% if category.description %}
<div id="{{ form_prefix }}category-info-{{ category.id }}">{{ category.description|localize|rich_text }}</div> <div id="{{ form_prefix }}category-info-{{ category.id }}">{{ category.description|localize|rich_text }}</div>
{% endif %} {% endif %}
{% else %} {% else %}
<section aria-labelledby="{{ form_prefix }}category-none"> <section aria-labelledby="{{ form_prefix }}category-none">
<h{{ headline_level|default:"3" }} id="{{ form_prefix }}category-none" class="h3 sr-only">{% trans "Uncategorized items" %}</h{{ headline_level|default:3 }}> <h3 id="{{ form_prefix }}category-none" class="sr-only">{% trans "Uncategorized items" %}</h3>
{% endif %} {% endif %}
{% for item in items %} {% for item in items %}
{% if item.has_variations %} {% if item.has_variations %}
@@ -43,7 +43,7 @@
</a> </a>
{% endif %} {% endif %}
<div class="product-description {% if item.picture %}with-picture{% endif %}"> <div class="product-description {% if item.picture %}with-picture{% endif %}">
<h{{ headline_level|default:3|add:1 }} class="h4" id="{{ form_prefix }}item-{{ item.pk }}-legend">{{ item.name }}</h{{ headline_level|default:3|add:1 }}> <h4 id="{{ form_prefix }}item-{{ item.pk }}-legend">{{ item.name }}</h4>
{% if item.description %} {% if item.description %}
<div id="{{ form_prefix }}item-{{ item.pk }}-description" class="product-description"> <div id="{{ form_prefix }}item-{{ item.pk }}-description" class="product-description">
{{ item.description|localize|rich_text }} {{ item.description|localize|rich_text }}
@@ -117,7 +117,7 @@
data-price="{% if event.settings.display_net_prices %}{{ var.display_price.net|unlocalize }}{% else %}{{ var.display_price.gross|unlocalize }}{% endif %}" data-price="{% if event.settings.display_net_prices %}{{ var.display_price.net|unlocalize }}{% else %}{{ var.display_price.gross|unlocalize }}{% endif %}"
{% endif %}> {% endif %}>
<div class="col-md-8 col-sm-6 col-xs-12"> <div class="col-md-8 col-sm-6 col-xs-12">
<h{{ headline_level|default:3|add:2 }} class="h5" id="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-legend">{{ var }}</h{{ headline_level|default:3|add:2 }}> <h5 id="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-legend">{{ var }}</h5>
{% if var.description %} {% if var.description %}
<div id="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-description" class="variation-description"> <div id="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-description" class="variation-description">
{{ var.description|localize|rich_text }} {{ var.description|localize|rich_text }}
@@ -261,7 +261,7 @@
</a> </a>
{% endif %} {% endif %}
<div class="product-description {% if item.picture %}with-picture{% endif %}"> <div class="product-description {% if item.picture %}with-picture{% endif %}">
<h{{ headline_level|default:3|add:1 }} class="h4" id="{{ form_prefix }}item-{{ item.pk }}-legend">{{ item.name }}</h{{ headline_level|default:3 }}> <h4 id="{{ form_prefix }}item-{{ item.pk }}-legend">{{ item.name }}</h4>
{% if item.description %} {% if item.description %}
<div id="{{ form_prefix }}item-{{ item.pk }}-description" class="product-description"> <div id="{{ form_prefix }}item-{{ item.pk }}-description" class="product-description">
{{ item.description|localize|rich_text }} {{ item.description|localize|rich_text }}
@@ -29,7 +29,7 @@ $(function () {
autofill = false; autofill = false;
}) })
function do_autofill() { $("input[name=itemvars]").change(function () {
if (autofill) { if (autofill) {
var names = []; var names = [];
$("input[name=itemvars]:checked").each(function () { $("input[name=itemvars]:checked").each(function () {
@@ -37,7 +37,5 @@ $(function () {
}); });
$("#id_name").val(names.join(', ')); $("#id_name").val(names.join(', '));
} }
} });
$("input[name=itemvars]").change(do_autofill);
do_autofill();
}); });
@@ -102,16 +102,15 @@
} }
} }
} }
.cross-selling .panel-body h3, .cross-selling .panel-body .h3 { .cross-selling .panel-body h3 {
font-size: 21px; font-size: 21px;
line-height: inherit; line-height: inherit;
margin-top: 20px; margin-top: 20px;
} }
.cross-selling .panel-body > *:first-child > h3:first-child, .cross-selling .panel-body > *:first-child > h3:first-child {
.cross-selling .panel-body > *:first-child > .h3:first-child {
margin-top: 0; margin-top: 0;
} }
.cross-selling .panel-body h3 small, .cross-selling .panel-body .h3 small { .cross-selling .panel-body h3 small {
padding-left: 20px; padding-left: 20px;
} }
@@ -53,7 +53,7 @@
&.variation label { &.variation label {
font-weight: normal; font-weight: normal;
} }
h4, .h4 { h4 {
font-size: inherit; font-size: inherit;
margin: 0; margin: 0;
line-height: inherit; line-height: inherit;
@@ -62,7 +62,7 @@
font-weight: bold; font-weight: bold;
} }
} }
h5, .h5 { h5 {
font-size: inherit; font-size: inherit;
font-weight: inherit; font-weight: inherit;
margin: 0; margin: 0;
+1 -2
View File
@@ -17,8 +17,7 @@ skip_glob = make_testdata.py,wsgi.py,bootstrap,celery_app.py,pretix/settings.py,
[tool:pytest] [tool:pytest]
DJANGO_SETTINGS_MODULE = tests.settings DJANGO_SETTINGS_MODULE = tests.settings
addopts = -rw addopts = --reruns 3 -rw
asyncio_default_fixture_loop_scope = function
filterwarnings = filterwarnings =
error error
ignore:.*invalid escape sequence.*: ignore:.*invalid escape sequence.*:
+2 -5
View File
@@ -123,7 +123,6 @@ TEST_ORDERPOSITION1_RES = {
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w", "secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": None, "addon_to": None,
"checkins": [], "checkins": [],
"print_logs": [],
"downloads": [], "downloads": [],
"answers": [], "answers": [],
"seat": None, "seat": None,
@@ -161,7 +160,6 @@ TEST_ORDERPOSITION2_RES = {
"secret": "sf4HZG73fU6kwddgjg2QOusFbYZwVKpK", "secret": "sf4HZG73fU6kwddgjg2QOusFbYZwVKpK",
"addon_to": None, "addon_to": None,
"checkins": [], "checkins": [],
"print_logs": [],
"downloads": [], "downloads": [],
"answers": [], "answers": [],
"seat": None, "seat": None,
@@ -199,7 +197,6 @@ TEST_ORDERPOSITION3_RES = {
"secret": "3u4ez6vrrbgb3wvezxhq446p548dt2wn", "secret": "3u4ez6vrrbgb3wvezxhq446p548dt2wn",
"addon_to": None, "addon_to": None,
"checkins": [], "checkins": [],
"print_logs": [],
"downloads": [], "downloads": [],
"answers": [], "answers": [],
"seat": None, "seat": None,
@@ -470,7 +467,7 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a
p3["addon_to"] = p1["id"] p3["addon_to"] = p1["id"]
# All items # All items
with django_assert_num_queries(24): with django_assert_num_queries(23):
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=positionid'.format( resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=positionid'.format(
organizer.slug, event.slug, clist_all.pk organizer.slug, event.slug, clist_all.pk
)) ))
@@ -1362,7 +1359,7 @@ def test_search(token_client, organizer, event, clist, clist_all, item, other_it
p1["id"] = order.positions.get(positionid=1).pk p1["id"] = order.positions.get(positionid=1).pk
p1["item"] = item.pk p1["item"] = item.pk
with django_assert_max_num_queries(18): with django_assert_max_num_queries(17):
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?search=z3fsn8jyu'.format( resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?search=z3fsn8jyu'.format(
organizer.slug, event.slug, clist_all.pk organizer.slug, event.slug, clist_all.pk
)) ))
-1
View File
@@ -163,7 +163,6 @@ TEST_ORDERPOSITION1_RES = {
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w", "secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": None, "addon_to": None,
"checkins": [], "checkins": [],
"print_logs": [],
"downloads": [], "downloads": [],
"answers": [], "answers": [],
"seat": None, "seat": None,
-1
View File
@@ -155,7 +155,6 @@ def test_giftcard_detail_expand(token_client, organizer, event, giftcard):
"addon_to": None, "addon_to": None,
"subevent": None, "subevent": None,
"checkins": [], "checkins": [],
"print_logs": [],
"downloads": [], "downloads": [],
"answers": [], "answers": [],
"tax_rule": None, "tax_rule": None,
+2 -3
View File
@@ -463,7 +463,6 @@ def test_order_create_simulate(token_client, organizer, event, item, quota, ques
'subevent': None, 'subevent': None,
'discount': None, 'discount': None,
'checkins': [], 'checkins': [],
'print_logs': [],
'downloads': [], 'downloads': [],
"valid_from": None, "valid_from": None,
"valid_until": None, "valid_until": None,
@@ -536,13 +535,13 @@ def test_order_create_positionids_addons_simulated(token_client, organizer, even
'attendee_name': 'Peter', 'attendee_name_parts': {'full_name': 'Peter', '_scheme': 'full'}, 'company': None, 'attendee_name': 'Peter', 'attendee_name_parts': {'full_name': 'Peter', '_scheme': 'full'}, 'company': None,
'street': None, 'zipcode': None, 'city': None, 'country': None, 'state': None, 'attendee_email': None, 'street': None, 'zipcode': None, 'city': None, 'country': None, 'state': None, 'attendee_email': None,
'voucher': None, 'tax_rate': '0.00', 'tax_value': '0.00', 'discount': None, 'voucher_budget_use': None, 'voucher': None, 'tax_rate': '0.00', 'tax_value': '0.00', 'discount': None, 'voucher_budget_use': None,
'addon_to': None, 'subevent': None, 'checkins': [], 'print_logs': [], 'downloads': [], 'answers': [], 'tax_rule': None, 'addon_to': None, 'subevent': None, 'checkins': [], 'downloads': [], 'answers': [], 'tax_rule': None,
'pseudonymization_id': 'PREVIEW', 'seat': None, 'canceled': False, 'valid_from': None, 'valid_until': None, 'blocked': None}, 'pseudonymization_id': 'PREVIEW', 'seat': None, 'canceled': False, 'valid_from': None, 'valid_until': None, 'blocked': None},
{'id': 0, 'order': '', 'positionid': 2, 'item': item.pk, 'variation': None, 'price': '23.00', {'id': 0, 'order': '', 'positionid': 2, 'item': item.pk, 'variation': None, 'price': '23.00',
'attendee_name': 'Peter', 'attendee_name_parts': {'full_name': 'Peter', '_scheme': 'full'}, 'company': None, 'attendee_name': 'Peter', 'attendee_name_parts': {'full_name': 'Peter', '_scheme': 'full'}, 'company': None,
'street': None, 'zipcode': None, 'city': None, 'country': None, 'state': None, 'attendee_email': None, 'street': None, 'zipcode': None, 'city': None, 'country': None, 'state': None, 'attendee_email': None,
'voucher': None, 'tax_rate': '0.00', 'tax_value': '0.00', 'discount': None, 'voucher_budget_use': None, 'voucher': None, 'tax_rate': '0.00', 'tax_value': '0.00', 'discount': None, 'voucher_budget_use': None,
'addon_to': 1, 'subevent': None, 'checkins': [], 'print_logs': [], 'downloads': [], 'answers': [], 'tax_rule': None, 'addon_to': 1, 'subevent': None, 'checkins': [], 'downloads': [], 'answers': [], 'tax_rule': None,
'pseudonymization_id': 'PREVIEW', 'seat': None, 'canceled': False, 'valid_from': None, 'valid_until': None, 'blocked': None} 'pseudonymization_id': 'PREVIEW', 'seat': None, 'canceled': False, 'valid_from': None, 'valid_until': None, 'blocked': None}
] ]
+12 -65
View File
@@ -76,7 +76,7 @@ def quota(event, item):
@pytest.fixture @pytest.fixture
def order(event, item, device, taxrule, question): def order(event, item, taxrule, question):
testtime = datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=datetime.timezone.utc) testtime = datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=datetime.timezone.utc)
event.plugins += ",pretix.plugins.stripe" event.plugins += ",pretix.plugins.stripe"
event.save() event.save()
@@ -137,13 +137,6 @@ def order(event, item, device, taxrule, question):
canceled=True, canceled=True,
positionid=2, positionid=2,
) )
op.print_logs.create(
device=device,
type="badge",
source="pretixpos",
info={"cashier": 1234},
datetime=datetime.datetime(2017, 12, 1, 12, 0, 0, tzinfo=datetime.timezone.utc),
)
op.answers.create(question=question, answer='S') op.answers.create(question=question, answer='S')
return o return o
@@ -207,19 +200,6 @@ TEST_ORDERPOSITION_RES = {
"addon_to": None, "addon_to": None,
"pseudonymization_id": "ABCDEFGHKL", "pseudonymization_id": "ABCDEFGHKL",
"checkins": [], "checkins": [],
"print_logs": [
{
"id": -1,
"device_id": -1,
"successful": True,
"datetime": "2017-12-01T12:00:00Z",
"source": "pretixpos",
"type": "badge",
"info": {
"cashier": 1234
},
}
],
"downloads": [], "downloads": [],
"seat": None, "seat": None,
"company": None, "company": None,
@@ -341,7 +321,7 @@ TEST_ORDER_RES = {
@pytest.mark.django_db @pytest.mark.django_db
def test_order_list_filter_subevent_date(token_client, device, organizer, event, order, item, taxrule, subevent, question): def test_order_list_filter_subevent_date(token_client, organizer, event, order, item, taxrule, subevent, question):
res = copy.deepcopy(TEST_ORDER_RES) res = copy.deepcopy(TEST_ORDER_RES)
with scopes_disabled(): with scopes_disabled():
res["positions"][0]["id"] = order.positions.first().pk res["positions"][0]["id"] = order.positions.first().pk
@@ -349,9 +329,6 @@ def test_order_list_filter_subevent_date(token_client, device, organizer, event,
p.subevent = subevent p.subevent = subevent
p.save() p.save()
fee = order.fees.first() fee = order.fees.first()
pl = p.print_logs.first()
res["positions"][0]["print_logs"][0]["id"] = pl.pk
res["positions"][0]["print_logs"][0]["device_id"] = device.device_id
res["positions"][0]["item"] = item.pk res["positions"][0]["item"] = item.pk
res["positions"][0]["subevent"] = subevent.pk res["positions"][0]["subevent"] = subevent.pk
res["positions"][0]["answers"][0]["question"] = question.pk res["positions"][0]["answers"][0]["question"] = question.pk
@@ -402,13 +379,11 @@ def test_order_list_filter_subevent_date(token_client, device, organizer, event,
@pytest.mark.django_db @pytest.mark.django_db
def test_order_list(token_client, organizer, event, order, item, taxrule, question, device): def test_order_list(token_client, organizer, event, order, item, taxrule, question):
res = dict(TEST_ORDER_RES) res = dict(TEST_ORDER_RES)
with scopes_disabled(): with scopes_disabled():
res["positions"][0]["id"] = order.positions.first().pk res["positions"][0]["id"] = order.positions.first().pk
res["fees"][0]["id"] = order.fees.first().pk res["fees"][0]["id"] = order.fees.first().pk
res["positions"][0]["print_logs"][0]["id"] = order.positions.first().print_logs.first().pk
res["positions"][0]["print_logs"][0]["device_id"] = device.device_id
res["positions"][0]["item"] = item.pk res["positions"][0]["item"] = item.pk
res["positions"][0]["answers"][0]["question"] = question.pk res["positions"][0]["answers"][0]["question"] = question.pk
res["last_modified"] = order.last_modified.isoformat().replace('+00:00', 'Z') res["last_modified"] = order.last_modified.isoformat().replace('+00:00', 'Z')
@@ -522,7 +497,6 @@ def test_order_detail(token_client, organizer, event, order, item, taxrule, ques
res = dict(TEST_ORDER_RES) res = dict(TEST_ORDER_RES)
with scopes_disabled(): with scopes_disabled():
res["positions"][0]["id"] = order.positions.first().pk res["positions"][0]["id"] = order.positions.first().pk
res["positions"][0]["print_logs"][0]["id"] = order.positions.first().print_logs.first().pk
res["fees"][0]["id"] = order.fees.first().pk res["fees"][0]["id"] = order.fees.first().pk
res["positions"][0]["item"] = item.pk res["positions"][0]["item"] = item.pk
res["fees"][0]["tax_rule"] = taxrule.pk res["fees"][0]["tax_rule"] = taxrule.pk
@@ -994,14 +968,12 @@ def test_orderposition_list(token_client, organizer, device, event, order, item,
var2 = item.variations.create(value="Children") var2 = item.variations.create(value="Children")
res = copy.copy(TEST_ORDERPOSITION_RES) res = copy.copy(TEST_ORDERPOSITION_RES)
op = order.positions.first() op = order.positions.first()
op.variation = var op.variation = var
op.save() op.save()
res["id"] = op.pk res["id"] = op.pk
res["item"] = item.pk res["item"] = item.pk
res["variation"] = var.pk res["variation"] = var.pk
res["answers"][0]["question"] = question.pk res["answers"][0]["question"] = question.pk
res["print_logs"][0]["id"] = op.print_logs.first().pk
res["print_logs"][0]["device_id"] = device.device_id
resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/'.format(organizer.slug, event.slug)) resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/'.format(organizer.slug, event.slug))
assert resp.status_code == 200 assert resp.status_code == 200
@@ -1100,7 +1072,7 @@ def test_orderposition_list(token_client, organizer, device, event, order, item,
'gate': None, 'gate': None,
'type': 'entry' 'type': 'entry'
}] }]
with django_assert_num_queries(16): with django_assert_num_queries(15):
resp = token_client.get( resp = token_client.get(
'/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=true'.format(organizer.slug, event.slug) '/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=true'.format(organizer.slug, event.slug)
) )
@@ -1135,7 +1107,6 @@ def test_orderposition_detail(token_client, organizer, event, order, item, quest
res = dict(TEST_ORDERPOSITION_RES) res = dict(TEST_ORDERPOSITION_RES)
with scopes_disabled(): with scopes_disabled():
op = order.positions.first() op = order.positions.first()
res["print_logs"][0]["id"] = op.print_logs.first().pk
res["id"] = op.pk res["id"] = op.pk
res["item"] = item.pk res["item"] = item.pk
res["answers"][0]["question"] = question.pk res["answers"][0]["question"] = question.pk
@@ -1200,30 +1171,6 @@ def test_orderposition_delete(token_client, organizer, event, order, item, quest
assert order.total == Decimal('23.25') assert order.total == Decimal('23.25')
@pytest.mark.django_db
def test_orderposition_printlog(token_client, team, organizer, event, order, item, question):
with scopes_disabled():
op = order.positions.first()
resp = token_client.post('/api/v1/organizers/{}/events/{}/orderpositions/{}/printlog/'.format(
organizer.slug, event.slug, op.pk
), data={
"datetime": "2023-09-04T12:23:45+02:00",
"source": "pretixscan",
"type": "badge",
"info": {
"cashier": 1234,
}
}, format='json')
assert resp.status_code == 201
with scopes_disabled():
l = op.print_logs.get(source="pretixscan")
assert l.type == "badge"
assert l.info == {"cashier": 1234}
assert l.api_token.team == team
assert l.datetime.isoformat() == "2023-09-04T10:23:45+00:00"
@pytest.mark.django_db @pytest.mark.django_db
def test_order_mark_paid_pending(token_client, organizer, event, order): def test_order_mark_paid_pending(token_client, organizer, event, order):
resp = token_client.post( resp = token_client.post(
@@ -1973,7 +1920,7 @@ def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_q
assert not resp.data['positions'][0].get('pdf_data') assert not resp.data['positions'][0].get('pdf_data')
# order list # order list
with django_assert_max_num_queries(31): with django_assert_max_num_queries(30):
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format( resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format(
organizer.slug, event.slug organizer.slug, event.slug
)) ))
@@ -1988,7 +1935,7 @@ def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_q
assert not resp.data['results'][0]['positions'][0].get('pdf_data') assert not resp.data['results'][0]['positions'][0].get('pdf_data')
# position list # position list
with django_assert_max_num_queries(34): with django_assert_max_num_queries(33):
resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/?pdf_data=true'.format( resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/?pdf_data=true'.format(
organizer.slug, event.slug organizer.slug, event.slug
)) ))
-1
View File
@@ -192,7 +192,6 @@ def test_medium_detail(token_client, organizer, event, medium, giftcard, custome
"addon_to": None, "addon_to": None,
"subevent": None, "subevent": None,
"checkins": [], "checkins": [],
"print_logs": [],
"downloads": [], "downloads": [],
"answers": [], "answers": [],
"tax_rule": None, "tax_rule": None,
+30 -8
View File
@@ -21,8 +21,10 @@
# #
import inspect import inspect
import os import os
import time
import pytest import pytest
from django.db import connection
from django.test import override_settings from django.test import override_settings
from django.utils import translation from django.utils import translation
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
@@ -84,7 +86,7 @@ def reset_locale():
@pytest.fixture @pytest.fixture
def fakeredis_client(monkeypatch): def fakeredis_client(monkeypatch):
worker_id = os.environ.get("PYTEST_XDIST_WORKER") worker_id = os.environ.get("PYTEST_XDIST_WORKER")
if worker_id and worker_id.startswith("gw"): if worker_id.startswith("gw"):
redis_port = 1000 + int(worker_id.replace("gw", "")) redis_port = 1000 + int(worker_id.replace("gw", ""))
else: else:
redis_port = 1000 redis_port = 1000
@@ -121,11 +123,31 @@ def fakeredis_client(monkeypatch):
yield redis yield redis
@pytest.fixture(autouse=True)
def set_lock_namespaces(request):
worker_id = os.environ.get("PYTEST_XDIST_WORKER") # XXXXXXXXXXXXXXXXXXX for test only
if worker_id and worker_id.startswith("gw"):
with override_settings(DATABASE_ADVISORY_LOCK_INDEX=int(worker_id.replace("gw", ""))): f = open("/tmp/test.txt","w")
yield
else: if os.environ.get("GITHUB_WORKFLOW", ""):
@pytest.fixture(autouse=True)
def ensure_healthy_connection(request, worker_id):
# We have no idea why this is neccessary. It shouldn't be, and it costs some performance.
# However, in ~August 2024 our tests became really flake on GitHub Actions (failing more than 80% of the time)
# for no apparent reason with some error messages related to PostgreSQL connection issues. This appears to
# work around it...
# Check if the test even has DB access
marker = request.node.get_closest_marker("django_db")
f.write(str(time.time())+"\t"+ str(worker_id)+"\t"+str(request.path)+"\t"+ str(request.module.__name__)+"\t"+ str(request.function.__name__)+"\tstart\n")
f.flush()
# Run actual test
yield yield
f.write(str(time.time())+"\t"+ str(worker_id)+"\t"+str(request.path)+"\t"+ str(request.module.__name__)+"\t"+ str(request.function.__name__)+"\tend\n")
f.flush()
# If yes, do a dummy query at the end of the test
#if marker:
# with connection.cursor() as cursor:
# cursor.execute("SELECT 1")