commit 17973e866b51faba553e5cae44a348d16af4b391 Author: Vincent Mahnke Date: Sat Nov 8 18:21:46 2025 +0100 feat: Initial commit diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..aa463dc --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @ZPascal diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..84c87d6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85e7c1d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1500756 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# Pretix Docker-Compose setup +The repository includes a [Pretix](https://pretix.eu/about/de/) docker-compose configuration for local development. + +## Usage + +You can execute `docker-compose up -d --build --force-recreate` to start and build all related containers. + +### Version information + +| **Version** | **Description** | +|:-----------:|:----------------------------------------------------------------:| +| 1.2.0 | Includes PostgreSQL 17 | +| 1.1.1 | Update the Alpine version and the allocated IPs of the databases | +| 1.1.0 | Includes PostgreSQL 16 | +| 1.0.0 | Includes PostgreSQL 13 | + +### Cronjobs + +It is possible to adapt the `pretixuser` crontab entries by modifying the [crontab](docker/pretix/crontab) file. + +## TLS setup + +You can specify the used TLS certificates by adapting the mounted [certificate](docker/pretix/files/config/ssl/domain.crt) and [key](docker/pretix/files/config/ssl/domain.key) e.g. from Let's Encrypt or generating new self-signed certificates by following the [manual](scripts/EXAMPLE-CERT-CREATION.md) and moving the generated files. It is also possible to adapt the [used](docker/pretix/nginx/nginx.conf) Nginx configuration. + +## Contribution +If you would like to contribute something, have an improvement request, or want to make a change inside the code, please open a pull request. + +## Support +If you need support, or you encounter a bug, please don't hesitate to open an issue. + +## Donations +If you want to support my work, I ask you to take an unusual action inside the open source community. Donate the money to a non-profit organization like Doctors Without Borders or the Children's Cancer Aid. I will continue to build tools because I like them, and I am passionate about developing and sharing applications. + +## License +This product is available under the Apache 2.0 license. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..db73089 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,56 @@ +version: '3' +services: + app: + container_name: pretix_app + build: + dockerfile: Dockerfile + context: ./docker/pretix + restart: always + depends_on: + - database + - cache + volumes: + - pretix_data:/data + - ./docker/pretix/pretix.cfg:/etc/pretix/pretix.cfg + - ./docker/pretix/nginx/nginx.conf:/etc/nginx/nginx.conf + - ./docker/pretix/crontab:/tmp/crontab + - ./plugin:/plugin + ports: + - "8081:80" + - "4434:443" + networks: + - backend + + database: + image: postgres:17-alpine3.22 + container_name: database + ports: + - "5432:5432" + environment: + - POSTGRES_USER=pretix + - POSTGRES_PASSWORD=pretix + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - backend + + cache: + image: redis:alpine3.22 + container_name: redis + ports: + - "6379:6379" + restart: always + networks: + - backend + +volumes: + postgres_data: + driver: local + pretix_data: + driver: local + +networks: + backend: + driver: bridge + driver_opts: + com.docker.network.bridge.host_binding_ipv4: "127.0.0.1" diff --git a/docker/pretix/Dockerfile b/docker/pretix/Dockerfile new file mode 100644 index 0000000..b228d44 --- /dev/null +++ b/docker/pretix/Dockerfile @@ -0,0 +1,19 @@ +FROM pretix/standalone:stable + +USER root + +ENV IMAGE_CRON_DIR="/image/cron" \ + IMAGE_CONFIG_DIR="/image/config" + +ADD files /image +COPY crontab /tmp/crontab + +RUN mv /image/supervisord/crond.conf /etc/supervisord/crond.conf && \ + pip install crontab && chmod 644 $IMAGE_CONFIG_DIR/ssl/*.crt && chmod +x $IMAGE_CRON_DIR/cron.py + +USER pretixuser + +EXPOSE 443 + +ENTRYPOINT ["pretix"] +CMD ["all"] \ No newline at end of file diff --git a/docker/pretix/crontab b/docker/pretix/crontab new file mode 100644 index 0000000..ffb17fe --- /dev/null +++ b/docker/pretix/crontab @@ -0,0 +1,24 @@ +# Edit this file to introduce tasks to be run by cron. +# +# Each task to run has to be defined through a single line +# indicating with different fields when the task will be run +# and what command to run for the task +# +# To define the time you can provide concrete values for +# minute (m), hour (h), day of month (dom), month (mon), +# and day of week (dow) or use '*' in these fields (for 'any'). +# +# Notice that tasks will be started based on the cron's system +# daemon's notion of time and timezones. +# +# Output of the crontab jobs (including errors) is sent through +# email to the user the crontab file belongs to (unless redirected). +# +# For example, you can run a backup of all your user accounts +# at 5 a.m every week with: +# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/ +# +# For more information see the manual pages of crontab(5) and cron(8) +# +# m h dom mon dow command +15,45 * * * * su pretixuser -c "PRETIX_CONFIG_FILE=/etc/pretix/pretix.cfg python -m pretix runperiodic" \ No newline at end of file diff --git a/docker/pretix/files/config/ssl/.placeholder b/docker/pretix/files/config/ssl/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/docker/pretix/files/config/ssl/domain.crt b/docker/pretix/files/config/ssl/domain.crt new file mode 100644 index 0000000..e69de29 diff --git a/docker/pretix/files/config/ssl/domain.key b/docker/pretix/files/config/ssl/domain.key new file mode 100644 index 0000000..e69de29 diff --git a/docker/pretix/files/cron/cron.py b/docker/pretix/files/cron/cron.py new file mode 100644 index 0000000..13fba51 --- /dev/null +++ b/docker/pretix/files/cron/cron.py @@ -0,0 +1,224 @@ +#!/usr/local/bin/python3 + +from crontab import CronTab +import argparse +import logging +import time +import subprocess +import sys +import signal +import os + + +def _parse_crontab(crontab_file: str) -> list: + """The method includes a functionality to parse the crontab file, and it returns a list of CronTab jobs + + Keyword arguments: + crontab_file -> Specify the inserted crontab file + """ + + logger = logging.getLogger("parser") + + logger.info(f"Reading crontab from {crontab_file}") + + if not os.path.isfile(crontab_file): + logger.error(f"Crontab {crontab_file} does not exist. Exiting!") + sys.exit(1) + + with open(crontab_file, "r") as crontab: + lines: list = crontab.readlines() + + logger.info(f"{len(lines)} lines read from crontab {crontab_file}") + + jobs: list = list() + + for i, line in enumerate(lines): + line: str = line.strip() + + if not line: + continue + + if line.startswith("#"): + continue + + logger.info(f"Parsing line {line}") + + expression: list = line.split(" ", 5) + cron_expression: str = " ".join(expression[0:5]) + + logger.info(f"Cron expression is {cron_expression}") + + try: + cron_entry = CronTab(cron_expression) + except ValueError as e: + logger.critical( + f"Unable to parse crontab. Line {i + 1}: Illegal cron expression {cron_expression}. Error message: {e}" + ) + sys.exit(1) + + command: str = expression[5] + + logger.info(f"Command is {command}") + + jobs.append([cron_entry, command]) + + if len(jobs) == 0: + logger.error( + "Specified crontab does not contain any scheduled execution. Exiting!" + ) + sys.exit(1) + + return jobs + + +def _get_next_executions(jobs: list): + """The method includes a functionality to extract the execution time and job itself from the submitted job list + + Keyword arguments: + jobs -> Specify the inserted list of jobs + """ + + logger = logging.getLogger("next-exec") + + scheduled_executions: tuple = tuple( + (x[1], int(x[0].next(default_utc=True)) + 1) for x in jobs + ) + + logger.debug(f"Next executions of scheduled are {scheduled_executions}") + + next_exec_time: int = int(min(scheduled_executions, key=lambda x: x[1])[1]) + + logger.debug(f"Next execution is in {next_exec_time} second(s)") + + next_commands: list = [x[0] for x in scheduled_executions if x[1] == next_exec_time] + + logger.debug( + f"Next commands to be executed in {next_exec_time} are {next_commands}" + ) + + return next_exec_time, next_commands + + +def _loop(jobs: list, test_mode: bool = False): + """The method includes a functionality to loop over all jobs inside the crontab file and execute them + + Keyword arguments: + jobs -> Specify the inserted jobs as list + test_mode -> Specify if you want to use the test mode or not (default False) + """ + + logger = logging.getLogger("loop") + + logger.info("Entering main loop") + + if test_mode is False: + while True: + sleep_time, commands = _get_next_executions(jobs) + + logger.debug(f"Sleeping for {sleep_time} second(s)") + + if sleep_time <= 1: + logger.debug("Sleep time <= 1 second, ignoring.") + time.sleep(1) + continue + + time.sleep(sleep_time) + + for command in commands: + _execute_command(command) + else: + sleep_time, commands = _get_next_executions(jobs) + + logger.debug(f"Sleeping for {sleep_time} second(s)") + + if sleep_time <= 1: + logger.debug("Sleep time <= 1 second, ignoring.") + time.sleep(1) + + time.sleep(sleep_time) + + for command in commands: + _execute_command(command) + + +def _execute_command(command: str): + """The method includes a functionality to execute a crontab command + + Keyword arguments: + command -> Specify the inserted command for the execution + """ + + logger = logging.getLogger("exec") + + logger.info(f"Executing command {command}") + + result = subprocess.run( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True + ) + + logger.info(f"Standard output: {result.stdout}") + logger.info(f"Standard error: {result.stderr}") + + +def _signal_handler(): + """The method includes a functionality for the signal handler to exit a process""" + + logger = logging.getLogger("signal") + logger.info("Exiting") + sys.exit(0) + + +def main(): + """The method includes a functionality to control and execute crontab entries + + Arguments: + -c -> Specify the inserted crontab file + -L -> Specify the inserted log file + -C -> Specify the if the output should be forwarded to the console + -l -> Specify the log level + """ + signal.signal(signal.SIGINT, _signal_handler) + signal.signal(signal.SIGTERM, _signal_handler) + + parser = argparse.ArgumentParser(description="cron") + parser.add_argument("-c", "--crontab", required=True, type=str) + logging_target = parser.add_mutually_exclusive_group(required=True) + logging_target.add_argument("-L", "--logfile", type=str) + logging_target.add_argument("-C", "--console", action="store_true") + parser.add_argument( + "-l", + "--loglevel", + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + default="INFO", + type=str, + ) + + args = parser.parse_args() + + log_level = getattr(logging, args.loglevel.upper(), logging.INFO) + + if args.console: + logging.basicConfig( + filemode="w", + level=log_level, + format="%(asctime)s %(name)-12s %(levelname)-8s %(message)s", + ) + else: + logging.basicConfig( + filename=args.logfile, + filemode="a+", + level=log_level, + format="%(asctime)s %(name)-12s %(levelname)-8s %(message)s", + ) + + logger = logging.getLogger("main") + + logger.info("Starting cron") + + jobs: list = _parse_crontab(args.crontab) + + _loop(jobs) + + +if __name__ == "__main__": + main() diff --git a/docker/pretix/files/supervisord/crond.conf b/docker/pretix/files/supervisord/crond.conf new file mode 100644 index 0000000..ffd9a56 --- /dev/null +++ b/docker/pretix/files/supervisord/crond.conf @@ -0,0 +1,7 @@ +[program:crond] +command = %(ENV_IMAGE_CRON_DIR)s/cron.py --crontab /tmp/crontab --loglevel INFO --logfile /var/log/crond.log +autostart = true +redirect_stderr = true +stdout_logfile = /var/log/crond.log +stdout_logfile_maxbytes = 1MB +stdout_logfile_backups = 2 \ No newline at end of file diff --git a/docker/pretix/nginx/nginx.conf b/docker/pretix/nginx/nginx.conf new file mode 100644 index 0000000..1f43036 --- /dev/null +++ b/docker/pretix/nginx/nginx.conf @@ -0,0 +1,85 @@ +user www-data www-data; +worker_processes auto; +pid /var/run/nginx.pid; +daemon off; +worker_rlimit_nofile 262144; + +events { + worker_connections 16384; + multi_accept on; + use epoll; +} + +http { + server_tokens off; + sendfile on; + charset utf-8; + tcp_nopush on; + tcp_nodelay on; + + log_format private '[$time_local] $host "$request" $status $body_bytes_sent'; + + types_hash_max_size 2048; + server_names_hash_bucket_size 64; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + add_header X-Content-Type-Options nosniff; + + access_log /var/log/nginx/access.log private; + error_log /var/log/nginx/error.log; + add_header Referrer-Policy same-origin; + + gzip on; + gzip_disable "msie6"; + gzip_types text/plain text/css application/json application/javascript application/x-javascript text/javascript text/xml application/xml application/rss+xml application/atom+xml application/rdf+xml image/svg+xml; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + + include /etc/nginx/conf.d/*.conf; + + server { + listen 80 backlog=4096 default_server; + listen [::]:80 ipv6only=on default_server; + server_name _; + + index index.php index.html; + root /var/www; + + location /media/ { + alias /data/media/; + expires 7d; + access_log off; + } + location ^~ /media/cachedfiles { + deny all; + return 404; + } + location ^~ /media/invoices { + deny all; + return 404; + } + location /static/ { + alias /pretix/src/pretix/static.dist/; + access_log off; + expires 365d; + add_header Cache-Control "public"; + add_header Access-Control-Allow-Origin "*"; + gzip on; + } + location / { + # Very important: + # proxy_pass http://unix:/tmp/pretix.sock:; + # is not the same as + # proxy_pass http://unix:/tmp/pretix.sock:/; + # In the latter case, nginx will apply its URL parsing, in the former it doesn't. + # There are situations in which pretix' API will deal with "file names" containing %2F%2F, which + # nginx will normalize to %2F, which can break ticket validation. + proxy_pass http://unix:/tmp/pretix.sock:; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + } + } +} \ No newline at end of file diff --git a/docker/pretix/pretix.cfg b/docker/pretix/pretix.cfg new file mode 100644 index 0000000..8ab507b --- /dev/null +++ b/docker/pretix/pretix.cfg @@ -0,0 +1,38 @@ +[pretix] +instance_name=localhost +url=http://localhost +currency=EUR +; DO NOT change the following value, it has to be set to the location of the +; directory *inside* the docker container +datadir=/data +registration=off + +[locale] +default=de +timezone=Europe/Berlin + +[database] +backend=postgresql +name=pretix +user=pretix +password=pretix +host=database + +[mail] +from=FROM_MAIL +host=MAIL_SERVER +user=USERNAME +password=FOOBAR +port=587 +tls=off +ssl=off + +[redis] +location=redis://cache/0 +; Remove the following line if you are unsure about your redis'security +; to reduce impact if redis gets compromised. +sessions=true + +[celery] +backend=redis://cache/1 +broker=redis://cache/2 \ No newline at end of file diff --git a/plugin/.gitignore b/plugin/.gitignore new file mode 100644 index 0000000..cc7fbd7 --- /dev/null +++ b/plugin/.gitignore @@ -0,0 +1,62 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg +.ropeproject/ + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +#Ipython Notebook +.ipynb_checkpoints diff --git a/plugin/.gitlab-ci.yml b/plugin/.gitlab-ci.yml new file mode 100644 index 0000000..1608db5 --- /dev/null +++ b/plugin/.gitlab-ci.yml @@ -0,0 +1,17 @@ +pypi: + 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: + - python -m build + - check-manifest . + - twine check dist/* + - twine upload dist/* + only: + - pypi + artifacts: + paths: + - dist/ diff --git a/plugin/.update-locales b/plugin/.update-locales new file mode 100755 index 0000000..1a1cbc5 --- /dev/null +++ b/plugin/.update-locales @@ -0,0 +1,37 @@ +#!/bin/sh +COMPONENTS=pretix/pretix-plugin-congressschedule pretix/pretix-plugin-congressschedule-js +DIR=pretix_congressschedule/locale +# Renerates .po files used for translating the plugin +set -e +set -x + +# Lock Weblate +for c in $COMPONENTS; do + wlc lock $c; +done + +# Push changes from Weblate to GitHub +for c in $COMPONENTS; do + wlc commit $c; +done + +# Pull changes from GitHub +git pull --rebase + +# Update po files itself +make localegen + +# Commit changes +git add $DIR/*/*/*.po +git add $DIR/*.pot + +git commit -s -m "Update po files +[CI skip]" + +# Push changes +git push + +# Unlock Weblate +for c in $COMPONENTS; do + wlc unlock $c; +done diff --git a/plugin/LICENSE b/plugin/LICENSE new file mode 100644 index 0000000..4f1c8b5 --- /dev/null +++ b/plugin/LICENSE @@ -0,0 +1,13 @@ +Copyright 2025 Vincent 'ViMaSter' Mahnke + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/plugin/MANIFEST.in b/plugin/MANIFEST.in new file mode 100644 index 0000000..2235582 --- /dev/null +++ b/plugin/MANIFEST.in @@ -0,0 +1,3 @@ +recursive-include pretix_congressschedule/static * +recursive-include pretix_congressschedule/templates * +recursive-include pretix_congressschedule/locale * diff --git a/plugin/Makefile b/plugin/Makefile new file mode 100644 index 0000000..d4e8db5 --- /dev/null +++ b/plugin/Makefile @@ -0,0 +1,10 @@ +all: localecompile +LNGS:=`find pretix_congressschedule/locale/ -mindepth 1 -maxdepth 1 -type d -printf "-l %f "` + +localecompile: + django-admin compilemessages + +localegen: + django-admin makemessages --keep-pot -i build -i dist -i "*egg*" $(LNGS) + django-admin makemessages -d djangojs --keep-pot -i build -i dist -i "*egg*" $(LNGS) + diff --git a/plugin/README.rst b/plugin/README.rst new file mode 100644 index 0000000..cd7f8f2 --- /dev/null +++ b/plugin/README.rst @@ -0,0 +1,46 @@ +pretix-congressschedule +======================= + +This is a plugin for `pretix`_. It generates a `c3voc-schema`_ compatible `schedule.xml` endpoint for events. + +Accessing schedule.xml +---------------------- + +1. Create an `event-series`_ in pretix; a singular event or non-event shop will not work, as products won't have required start and end times associated with them + +2. Visit `/api/v1/event/{organizationSlug}/{eventSlug}/schedule.xml` and replace `{organizationSlug}` and `{eventSlug}` with the respective slugs + +3. Receive either a 200 status code with an XML document adhering to `schedule.xml.xsd`_ or a 400 error code with additional information inside `` + + +Development setup +^^^^^^^^^^^^^^^^^ + +1. Make sure that you have a working `pretix development setup`_. + +2. Clone this repository, eg to ``local/pretix-congressschedule``. + +3. Activate the virtual environment you use for pretix development. + +4. Execute ``pip install -e .`` within this directory to register this application with pretix's plugin registry. + +5. Execute ``make`` within this directory to compile translations. + +6. Restart your local pretix server. You can now use the plugin from this repository for your events by enabling it in + the 'plugins' tab in the settings. + + +License +------- + +Copyright 2025 Vincent 'ViMaSter' Mahnke + +Released under the terms of the Apache License 2.0 + + + +.. _pretix: https://github.com/pretix/pretix +.. _pretix development setup: https://docs.pretix.eu/en/latest/development/setup.html +.. _c3voc-schema: https://c3voc.de/wiki/schedule#schedule_xml +.. _schedule.xml.xsd: https://c3voc.de/schedule/schema.xsd +.. _event-series: https://docs.pretix.eu/guides/event-series/?h=dates#how-to-create-an-event-series diff --git a/plugin/pretix_congressschedule/__init__.py b/plugin/pretix_congressschedule/__init__.py new file mode 100644 index 0000000..5becc17 --- /dev/null +++ b/plugin/pretix_congressschedule/__init__.py @@ -0,0 +1 @@ +__version__ = "1.0.0" diff --git a/plugin/pretix_congressschedule/api.py b/plugin/pretix_congressschedule/api.py new file mode 100644 index 0000000..5f457cf --- /dev/null +++ b/plugin/pretix_congressschedule/api.py @@ -0,0 +1,193 @@ +from django.http import HttpResponse +from rest_framework import views +from pretix.base.models import Event, SubEvent +import xml.etree.ElementTree as ET +from collections import defaultdict +from datetime import timedelta +import uuid +import re + +from . import __version__ + +class CongressScheduleView(views.APIView): + def get(self, request, organizer, event, *args, **kwargs): + try: + ev = Event.objects.get(organizer__slug=organizer, slug=event) + except Event.DoesNotExist: + return HttpResponse(b'Event not found', status=404, content_type='application/xml') + + if not ev.has_subevents: + return HttpResponse( + b'Event is not an event-series: https://docs.pretix.eu/guides/event-series/?h=dates#how-to-create-an-event-series', + status=400, + content_type='application/xml' + ) + + subs = SubEvent.objects.filter(event=ev).order_by('date_from') + + root = ET.Element('schedule') + + gen = ET.SubElement(root, 'generator') + gen.set('name', 'pretix-congressschedule') + gen.set('version', __version__) + + try: + feed_url = request.build_absolute_uri() + ET.SubElement(root, 'url').text = feed_url + except Exception: + pass + + # Version string – keep simple and stable per event + ET.SubElement(root, 'version').text = f"{ev.slug}-v1" + + conf = ET.SubElement(root, 'conference') + conf_title = ev.name.localize(ev.settings.locale) if hasattr(ev.name, 'localize') else str(ev.name) + ET.SubElement(conf, 'title').text = conf_title or str(ev.slug) + acronym = f"{organizer}_{event}".lower() + ET.SubElement(conf, 'acronym').text = acronym + + # start/end/days based on subevents if available, else fall back to event + all_starts = [se.date_from for se in subs if se.date_from] + all_ends = [se.date_to for se in subs if se.date_to] + + if all_starts: + ET.SubElement(conf, 'start').text = min(all_starts).isoformat() + if all_ends: + ET.SubElement(conf, 'end').text = max(all_ends).isoformat() + + # days count – unique calendar days from subevents + unique_days = sorted({(se.date_from.date() if se.date_from else None) for se in subs} - {None}) + if unique_days: + ET.SubElement(conf, 'days').text = str(len(unique_days)) + + # time zone name – try Event.timezone or settings + tz_name = getattr(ev, 'timezone', None) or getattr(ev.settings, 'timezone', None) + if tz_name: + tz_text = tz_name if isinstance(tz_name, str) else str(tz_name) + ET.SubElement(conf, 'time_zone_name').text = tz_text + + # Group subevents into days and rooms + # days: {date -> {room_name -> [subevents]}} + days: dict = defaultdict(lambda: defaultdict(list)) + + def get_room_name(se): + # Try SubEvent.location if present, else fallback to `Main` + loc = getattr(se, 'location', None) + if hasattr(loc, 'localize'): + try: + txt = loc.localize(ev.settings.locale) + except Exception: + txt = str(loc) + else: + txt = str(loc) if loc else '' + return (txt or 'Main').strip() or 'Main' + + for se in subs: + if not se.date_from: + # Skip entries without a start + continue + day_key = se.date_from.date() + room = get_room_name(se) + days[day_key][room].append(se) + + # Emit elements in chronological order + for day_index, (day_date, rooms) in enumerate(sorted(days.items()), start=1): + # Compute day start/end from all events this day + starts = [se.date_from for r in rooms.values() for se in r if se.date_from] + ends = [se.date_to for r in rooms.values() for se in r if se.date_to] + day_start = min(starts) if starts else None + # If end is missing for any, approximate using +0 duration => start + if ends: + day_end = max(ends) + else: + day_end = (day_start + timedelta(minutes=0)) if day_start else None + + day_el = ET.SubElement(root, 'day') + if day_date: + day_el.set('date', day_date.isoformat()) + if day_start: + day_el.set('start', day_start.isoformat()) + if day_end: + day_el.set('end', day_end.isoformat()) + day_el.set('index', str(day_index)) + + # Emit containers + for room_name, events_in_room in sorted(rooms.items(), key=lambda x: x[0].lower()): + room_el = ET.SubElement(day_el, 'room') + room_el.set('name', room_name) + # Optional guid on room – stable UUID5 based on names + room_el.set('guid', str(uuid.uuid5(uuid.NAMESPACE_DNS, f"room:{organizer}:{event}:{room_name}"))) + + # Emit each in chronological order within the room + for se in sorted(events_in_room, key=lambda s: s.date_from or 0): + ev_el = ET.SubElement(room_el, 'event') + ev_el.set('id', str(se.pk)) + ev_el.set('guid', str(uuid.uuid5(uuid.NAMESPACE_DNS, f"subevent:{ev.pk}:{se.pk}"))) + + # Helper: localize strings + def _localize(val): + if hasattr(val, 'localize'): + try: + return val.localize(ev.settings.locale) + except Exception: + return str(val) + return str(val) if val is not None else '' + + # Required children according to schema + ET.SubElement(ev_el, 'room').text = room_name + title = _localize(se.name) + ET.SubElement(ev_el, 'title').text = title + ET.SubElement(ev_el, 'subtitle').text = '' + ET.SubElement(ev_el, 'type').text = 'subevent' + + # date (full datetime with TZ) + if se.date_from: + ET.SubElement(ev_el, 'date').text = se.date_from.isoformat() + + # start (HH:MM or HH:MM:SS) + if se.date_from: + ET.SubElement(ev_el, 'start').text = se.date_from.strftime('%H:%M') + + # duration from date_to - date_from + dur_txt = '00:00' + if se.date_from and se.date_to and se.date_to >= se.date_from: + delta: timedelta = se.date_to - se.date_from + total_seconds = int(delta.total_seconds()) + hours = total_seconds // 3600 + minutes = (total_seconds % 3600) // 60 + seconds = total_seconds % 60 + # prefer HH:MM if no seconds, else HH:MM:SS + if seconds == 0: + dur_txt = f"{hours:02d}:{minutes:02d}" + else: + dur_txt = f"{hours:02d}:{minutes:02d}:{seconds:02d}" + ET.SubElement(ev_el, 'duration').text = dur_txt + + ET.SubElement(ev_el, 'abstract').text = '' + + # slug (pattern: "[a-z0-9_]{4,}-[a-z0-9\-_]{4,}") + def slugify(text: str) -> str: + text = (text or '').lower() + text = re.sub(r'\s+', '-', text) + text = re.sub(r'[^a-z0-9\-_]', '', text) + text = text.strip('-_') + return text or 'item' + + base = f"{organizer}_{event}".lower() + second = slugify(title) + if len(second) < 4: + second = f"{second}-{se.pk}" + ET.SubElement(ev_el, 'slug').text = f"{base}-{second}" + + # track – use room name as a simple track assignment + ET.SubElement(ev_el, 'track').text = slugify(room_name) or 'general' + + # Optional elements: keep minimal but include language if available + lang = getattr(ev.settings, 'locale', None) + if lang: + ET.SubElement(ev_el, 'language').text = str(lang) + + # Leave optional complex children (persons, recording, links, attachments) empty for now + + xml_bytes = ET.tostring(root, encoding='utf-8', xml_declaration=True) + return HttpResponse(xml_bytes, content_type='application/xml') \ No newline at end of file diff --git a/plugin/pretix_congressschedule/apps.py b/plugin/pretix_congressschedule/apps.py new file mode 100644 index 0000000..b756542 --- /dev/null +++ b/plugin/pretix_congressschedule/apps.py @@ -0,0 +1,43 @@ +from django.apps import AppConfig +from django.utils.functional import cached_property +from django.utils.translation import gettext_lazy + +from . import __version__ + + +class PassbookApp(AppConfig): + name = "pretix_congressschedule" + verbose_name = "Congress Schedule" + + class PretixPluginMeta: + name = gettext_lazy("Congress Schedule") + author = "Vincent Mahnke" + description = gettext_lazy("Provides passbook tickets for pretix") + category = "API" + visible = True + featured = True + version = __version__ + compatibility = "pretix>=4.17.0" + + def ready(self): + from . import signals # NOQA + + @cached_property + def compatibility_errors(self): + import shutil + + errs = [] + if not shutil.which("openssl"): + errs.append("The OpenSSL binary is not installed or not in the PATH.") + return errs + + @cached_property + def compatibility_warnings(self): + errs = [] + try: + from PIL import Image # NOQA + except ImportError: + errs.append( + "Pillow is not installed on this system, which is required for converting and scaling images." + ) + return errs diff --git a/plugin/pretix_congressschedule/locale/de/LC_MESSAGES/django.po b/plugin/pretix_congressschedule/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..0e651c1 --- /dev/null +++ b/plugin/pretix_congressschedule/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,19 @@ +# pretix-congressschedule +# Copyright (C) 2025 Vincent 'ViMaSter' Mahnke +# This file is distributed under the same license as the pretix-congressschedule package. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-08-16 15:52+0200\n" +"PO-Revision-Date: 2022-03-21 15:45+0000\n" +"Last-Translator: Vincent 'ViMaSter' Mahnke \n" +"Language-Team: German \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.11.2\n" diff --git a/plugin/pretix_congressschedule/locale/de/LC_MESSAGES/djangojs.po b/plugin/pretix_congressschedule/locale/de/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..7eceb08 --- /dev/null +++ b/plugin/pretix_congressschedule/locale/de/LC_MESSAGES/djangojs.po @@ -0,0 +1,19 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-08-16 15:52+0200\n" +"PO-Revision-Date: 2017-05-10 13:48+0200\n" +"Last-Translator: Vincent 'ViMaSter' Mahnke \n" +"Language-Team: \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.0.1\n" diff --git a/plugin/pretix_congressschedule/locale/de_Informal/.gitkeep b/plugin/pretix_congressschedule/locale/de_Informal/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/plugin/pretix_congressschedule/locale/de_Informal/LC_MESSAGES/django.po b/plugin/pretix_congressschedule/locale/de_Informal/LC_MESSAGES/django.po new file mode 100644 index 0000000..775b229 --- /dev/null +++ b/plugin/pretix_congressschedule/locale/de_Informal/LC_MESSAGES/django.po @@ -0,0 +1,19 @@ +# pretix-congressschedule +# Copyright (C) 2025 Vincent 'ViMaSter' Mahnke +# This file is distributed under the same license as the pretix-congressschedule package. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-08-16 15:52+0200\n" +"PO-Revision-Date: 2022-03-21 15:45+0000\n" +"Last-Translator: Vincent 'ViMaSter' Mahnke \n" +"Language-Team: German (informal) \n" +"Language: de_Informal\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.11.2\n" \ No newline at end of file diff --git a/plugin/pretix_congressschedule/locale/de_Informal/LC_MESSAGES/djangojs.po b/plugin/pretix_congressschedule/locale/de_Informal/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..34aebb6 --- /dev/null +++ b/plugin/pretix_congressschedule/locale/de_Informal/LC_MESSAGES/djangojs.po @@ -0,0 +1,19 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-08-16 15:52+0200\n" +"PO-Revision-Date: 2017-05-10 13:48+0200\n" +"Last-Translator: Vincent 'ViMaSter' Mahnke \n" +"Language-Team: \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.0.1\n" \ No newline at end of file diff --git a/plugin/pretix_congressschedule/locale/django.pot b/plugin/pretix_congressschedule/locale/django.pot new file mode 100644 index 0000000..a174392 --- /dev/null +++ b/plugin/pretix_congressschedule/locale/django.pot @@ -0,0 +1,26 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-08-16 15:52+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: pretix_congressschedule/__init__.py:11 +msgid "Congress Schedule" +msgstr "" + +#: pretix_congressschedule/__init__.py:13 +msgid "Provides passbook tickets for pretix" +msgstr "" \ No newline at end of file diff --git a/plugin/pretix_congressschedule/locale/djangojs.pot b/plugin/pretix_congressschedule/locale/djangojs.pot new file mode 100644 index 0000000..b1c4970 --- /dev/null +++ b/plugin/pretix_congressschedule/locale/djangojs.pot @@ -0,0 +1,32 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-08-16 15:52+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: pretix_congressschedule/static/pretix_congressschedule/geosuggest.js:4 +msgid "Loading suggested geolocations…" +msgstr "" + +#: pretix_congressschedule/static/pretix_congressschedule/geosuggest.js:13 +msgid "" +"Click on one of the following suggestions to fill in the coordinates " +"automatically:" +msgstr "" + +#: pretix_congressschedule/static/pretix_congressschedule/geosuggest.js:35 +msgid "Error while loading suggested geolocations." +msgstr "" diff --git a/plugin/pretix_congressschedule/signals.py b/plugin/pretix_congressschedule/signals.py new file mode 100644 index 0000000..e69de29 diff --git a/plugin/pretix_congressschedule/urls.py b/plugin/pretix_congressschedule/urls.py new file mode 100644 index 0000000..4f9c59d --- /dev/null +++ b/plugin/pretix_congressschedule/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from .api import CongressScheduleView + +urlpatterns = [ + path( + 'api/v1/event///schedule.xml', + CongressScheduleView.as_view(), + name='schedule-xml', + ), +] \ No newline at end of file diff --git a/plugin/pretixplugin.toml b/plugin/pretixplugin.toml new file mode 100644 index 0000000..7a0a5f0 --- /dev/null +++ b/plugin/pretixplugin.toml @@ -0,0 +1,10 @@ +# This file is used by the pretix team internally to coordinate releases of this plugin +[plugin] +package = "pretix-congressschedule" +modules = [ "pretix_congressschedule" ] +marketplace_name = "congressschedule" +pypi = true +repository_servers = { origin = "github.com", gitlab = "code.rami.io" } +tag_targets = [ "origin", "gitlab" ] +branch_targets = [ "origin/master", "gitlab/master", "f:gitlab/pypi" ] + diff --git a/plugin/pyproject.toml b/plugin/pyproject.toml new file mode 100644 index 0000000..e194848 --- /dev/null +++ b/plugin/pyproject.toml @@ -0,0 +1,39 @@ +[project] +name = "pretix-congressschedule" +dynamic = ["version"] +description = "c3voc schedule-compatible schedule.xml endpoint" +readme = "README.rst" +requires-python = ">=3.9" +license = {file = "LICENSE"} +keywords = ["pretix"] +authors = [ + {name = "Vincent Mahnke", email = "pretix-congress@vincent.mahn.ke"}, +] +maintainers = [ + {name = "Vincent Mahnke", email = "pretix-congress@vincent.mahn.ke"}, +] + +[project.entry-points."pretix.plugin"] +congressschedule = "pretix_congressschedule:PretixPluginMeta" + +[project.entry-points."distutils.commands"] +build = "pretix_plugin_build.build:CustomBuild" + +[build-system] +requires = [ + "setuptools", + "pretix-plugin-build" +] + +[project.urls] +homepage = "https://git.hamburg.ccc.de/ViMaSter/pretix-congressschedule" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.dynamic] +version = {attr = "pretix_congressschedule.__version__"} + +[tool.setuptools.packages.find] +include = ["pretix*"] +namespaces = false diff --git a/plugin/pytest.ini b/plugin/pytest.ini new file mode 100644 index 0000000..ccf8194 --- /dev/null +++ b/plugin/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +DJANGO_SETTINGS_MODULE=pretix.testutils.settings diff --git a/plugin/setup.cfg b/plugin/setup.cfg new file mode 100644 index 0000000..906fa08 --- /dev/null +++ b/plugin/setup.cfg @@ -0,0 +1,46 @@ +[flake8] +ignore = N802,W503,E402 +max-line-length = 160 +exclude = migrations,.ropeproject,static,_static,build,setup.py + +[isort] +combine_as_imports = true +default_section = THIRDPARTY +include_trailing_comma = true +known_third_party = pretix +known_standard_library = typing +multi_line_output = 3 +skip = setup.py +use_parentheses = True +force_grid_wrap = 0 +line_length = 88 +known_first_party = pretix_congressschedule + +[tool:pytest] +DJANGO_SETTINGS_MODULE = pretix.testutils.settings + +[coverage:run] +source = pretix_adyen +omit = */migrations/*,*/urls.py,*/tests/* + +[coverage:report] +exclude_lines = + pragma: no cover + def __str__ + der __repr__ + if settings.DEBUG + NOQA + NotImplementedError + +[check-manifest] +ignore = + .update-locales + .update-locales.sh + .gitlab-ci.yml + .install-hooks.sh + pretixplugin.toml + Makefile + pytest.ini + manage.py + tests/* + diff --git a/plugin/setup.py b/plugin/setup.py new file mode 100644 index 0000000..6068493 --- /dev/null +++ b/plugin/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() diff --git a/scripts/EXAMPLE-CERT-CREATION.md b/scripts/EXAMPLE-CERT-CREATION.md new file mode 100644 index 0000000..1cbb6b6 --- /dev/null +++ b/scripts/EXAMPLE-CERT-CREATION.md @@ -0,0 +1,21 @@ +# Example of the cert creation for the Nginx setup + +## Creation + +Please execute the following script `bash create-tls-certs.sh` to create all necessary certificates for the complete setup of all related components. + +## Adaptation + +Please adjust the configuration files inside the [config](./config) folder and adapt the corresponding values for the req_distinguished_names and subjectAltNames based on your organisation and configuration. You can find [here](https://support.dnsimple.com/articles/what-is-common-name/) and [here](https://learn.microsoft.com/en-us/azure/application-gateway/self-signed-certificates) more information about the corresponding values and CA certificates in general. + +## Ca Certificates + +### Nginx + +Describes the Certificate Authority (certificate & key) for the Nginx server. + +## Server Certificates + +### Nginx + +Describes the server certificate and key for the Nginx server, and it's signed by the Nginx CA. \ No newline at end of file diff --git a/scripts/certs/.placeholder b/scripts/certs/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/scripts/config/ca_nginx.conf b/scripts/config/ca_nginx.conf new file mode 100644 index 0000000..f9e047d --- /dev/null +++ b/scripts/config/ca_nginx.conf @@ -0,0 +1,20 @@ +[req] +distinguished_name = req_distinguished_name +default_bits = 4096 +prompt = no +default_md = sha256 + +[req_distinguished_name] +C = DE +ST = Baden-Wuerttemberg +L = Mannheim +O = TheIOTStudio +CN = Pretix Nginx CA +emailAddress = info@theiotstudio.com + +[ext] +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid:always,issuer +basicConstraints = critical, CA:TRUE, pathlen:3 +keyUsage = critical, cRLSign, keyCertSign +nsCertType = sslCA, emailCA \ No newline at end of file diff --git a/scripts/config/server_nginx.conf b/scripts/config/server_nginx.conf new file mode 100644 index 0000000..3d981e0 --- /dev/null +++ b/scripts/config/server_nginx.conf @@ -0,0 +1,19 @@ +[req] +distinguished_name = req_distinguished_name +req_extensions = v3_req +default_bits = 4096 +prompt = no +default_md = sha256 + +[req_distinguished_name] +C = DE +ST = Baden-Wuerttemberg +L = Mannheim +O = TheIOTStudio +CN = Pretix Nginx Server +emailAddress = info@theiotstudio.com + +[v3_req] +keyUsage = keyEncipherment, dataEncipherment, digitalSignature +extendedKeyUsage = serverAuth, clientAuth +subjectAltName=IP: or DNS: \ No newline at end of file diff --git a/scripts/create-tls-certs.sh b/scripts/create-tls-certs.sh new file mode 100755 index 0000000..823d930 --- /dev/null +++ b/scripts/create-tls-certs.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# Switch the directory +echo "Switch the directory" +path=$(pwd)/config +cd certs + +# Create the Nginx ca +echo "Create the Nginx ca" +openssl req -new -x509 -sha256 -newkey rsa:4096 -nodes -keyout ca_nginx.key -out ca_nginx.crt -days 3650 \ +-extensions ext \ +-config $path/ca_nginx.conf + +# Create the server certificates +echo "Create the Nginx server certificates" +openssl genrsa -out nginx.key 4096 +openssl req -new -key nginx.key -out nginx.csr -extensions v3_req -config $path/server_nginx.conf +openssl x509 -inform pem -req -days 1825 -in nginx.csr -CA ca_nginx.crt -CAkey ca_nginx.key -CAcreateserial -out nginx.crt -extensions v3_req -extfile $path/server_nginx.conf \ No newline at end of file