pretix-map 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pretix_map-0.0.3.dist-info/METADATA +193 -0
- pretix_map-0.0.3.dist-info/RECORD +45 -0
- pretix_map-0.0.3.dist-info/WHEEL +5 -0
- pretix_map-0.0.3.dist-info/entry_points.txt +5 -0
- pretix_map-0.0.3.dist-info/licenses/LICENSE +15 -0
- pretix_map-0.0.3.dist-info/top_level.txt +1 -0
- pretix_mapplugin/__init__.py +1 -0
- pretix_mapplugin/apps.py +28 -0
- pretix_mapplugin/geocoding.py +113 -0
- pretix_mapplugin/locale/de/LC_MESSAGES/django.mo +0 -0
- pretix_mapplugin/locale/de/LC_MESSAGES/django.po +12 -0
- pretix_mapplugin/locale/de_Informal/.gitkeep +0 -0
- pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.mo +0 -0
- pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.po +12 -0
- pretix_mapplugin/management/__init__.py +0 -0
- pretix_mapplugin/management/commands/__init__.py +0 -0
- pretix_mapplugin/management/commands/geocode_existing_orders.py +167 -0
- pretix_mapplugin/migrations/__init__.py +0 -0
- pretix_mapplugin/models.py +27 -0
- pretix_mapplugin/signals.py +73 -0
- pretix_mapplugin/static/pretix_mapplugin/.gitkeep +0 -0
- pretix_mapplugin/static/pretix_mapplugin/css/salesmap.css +19 -0
- pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js +378 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.Default.css +60 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.css +14 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/layers-2x.png +0 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/layers.png +0 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-icon-2x.png +0 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-icon.png +0 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-shadow.png +0 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-heat.js +11 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.esm.js +14419 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.esm.js.map +1 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.js +14512 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.js.map +1 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.css +661 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.js +6 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.js.map +1 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js +3 -0
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js.map +1 -0
- pretix_mapplugin/tasks.py +74 -0
- pretix_mapplugin/templates/pretix_mapplugin/.gitkeep +0 -0
- pretix_mapplugin/templates/pretix_mapplugin/map_page.html +45 -0
- pretix_mapplugin/urls.py +21 -0
- pretix_mapplugin/views.py +163 -0
@@ -0,0 +1,193 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: pretix-map
|
3
|
+
Version: 0.0.3
|
4
|
+
Summary: An overview map of the catchment area of previous orders. Measured by postcode
|
5
|
+
Author-email: MarkenJaden <jjsch1410@gmail.com>
|
6
|
+
Maintainer-email: MarkenJaden <jjsch1410@gmail.com>
|
7
|
+
License: Apache
|
8
|
+
Project-URL: homepage, https://github.com/MarkenJaden/pretix-map
|
9
|
+
Project-URL: repository, https://github.com/MarkenJaden/pretix-map
|
10
|
+
Keywords: pretix
|
11
|
+
Description-Content-Type: text/x-rst
|
12
|
+
License-File: LICENSE
|
13
|
+
Requires-Dist: geopy
|
14
|
+
Dynamic: license-file
|
15
|
+
|
16
|
+
Map-Plugin
|
17
|
+
==========================
|
18
|
+
|
19
|
+
This is a plugin for `pretix`_.
|
20
|
+
|
21
|
+
It provides an overview map visualizing the geographic location of attendees based on the addresses provided in their orders. The plugin automatically geocodes order addresses upon payment and displays the locations on an interactive map within the event control panel.
|
22
|
+
|
23
|
+
Features:
|
24
|
+
|
25
|
+
* Automatic geocoding of paid order addresses using a configured geocoding service.
|
26
|
+
* Interactive map display (Leaflet) showing locations as clustered pins or a heatmap.
|
27
|
+
* Option to toggle between pin view and heatmap view.
|
28
|
+
* Pins show tooltips with Order Code, Date, and Item Count on hover.
|
29
|
+
* Clicking a pin navigates directly to the corresponding order details page.
|
30
|
+
* Adds a "Sales Map" link to the event navigation sidebar.
|
31
|
+
* Includes a management command to geocode orders placed *before* the plugin was installed or configured.
|
32
|
+
|
33
|
+
Requirements
|
34
|
+
------------
|
35
|
+
|
36
|
+
* A working `pretix installation`_.
|
37
|
+
* A **Celery worker** configured and running for your Pretix instance. This is essential for the background geocoding tasks.
|
38
|
+
* Access to a **Geocoding Service**. This plugin requires configuration to use an external service to convert addresses to latitude/longitude coordinates. See the **Configuration** section below.
|
39
|
+
|
40
|
+
|
41
|
+
Installation (Production)
|
42
|
+
--------------------------
|
43
|
+
|
44
|
+
1. Ensure you meet the requirements above, especially a running Celery worker.
|
45
|
+
2. Activate the virtual environment used for your Pretix installation.
|
46
|
+
3. Install the plugin via pip:
|
47
|
+
|
48
|
+
.. code-block:: bash
|
49
|
+
|
50
|
+
pip install pretix-map-plugin
|
51
|
+
|
52
|
+
*(Note: If the plugin is not on PyPI yet, you might need to install from the git repository URL)*
|
53
|
+
4. Configure the required geocoding service settings in your `pretix.cfg` file (see **Configuration** below).
|
54
|
+
5. Restart your Pretix webserver (`gunicorn`/`uwsgi`) **and** your Pretix Celery worker(s).
|
55
|
+
6. Log in to your Pretix backend and go to Organizer Settings -> Plugins. Enable the "Sales Map" plugin for the desired organizer(s).
|
56
|
+
7. Go into an event, then navigate to Event Settings -> Plugins and ensure the "Sales Map" plugin is checked (enabled) for that event.
|
57
|
+
|
58
|
+
|
59
|
+
Configuration (`pretix.cfg`)
|
60
|
+
------------------------------
|
61
|
+
|
62
|
+
This plugin requires configuration in your `pretix.cfg` file to specify which geocoding service to use. Add a section `[pretix_mapplugin]` if it doesn't exist.
|
63
|
+
|
64
|
+
**Required Setting (Choose ONE method):**
|
65
|
+
|
66
|
+
* **Method 1: Nominatim (OpenStreetMap - Free, Requires User-Agent)**
|
67
|
+
Nominatim is a free geocoding service based on OpenStreetMap data. It has usage policies that **require** you to set a descriptive User-Agent header, typically including your application name and contact email. Failure to do so may result in your IP being blocked.
|
68
|
+
|
69
|
+
.. code-block:: ini
|
70
|
+
|
71
|
+
[pretix_mapplugin]
|
72
|
+
# REQUIRED for Nominatim: Set a descriptive User-Agent including application name and contact info.
|
73
|
+
# Replace with your actual details! See: https://operations.osmfoundation.org/policies/nominatim/
|
74
|
+
nominatim_user_agent=YourTicketingSite/1.0 (Contact: admin@yourdomain.com) pretix-map-plugin/1.0
|
75
|
+
|
76
|
+
* **Method 2: Other Geocoding Services (e.g., Google, Mapbox - API Key likely needed)**
|
77
|
+
*(This example assumes you have modified `tasks.py` to use a different geocoder like GeoPy with GoogleV3. Adjust the setting name and value based on your implementation.)*
|
78
|
+
|
79
|
+
.. code-block:: ini
|
80
|
+
|
81
|
+
[pretix_mapplugin]
|
82
|
+
# Example for Google Geocoding API (if implemented in tasks.py)
|
83
|
+
# google_geocoding_api_key=YOUR_GOOGLE_GEOCODING_API_KEY
|
84
|
+
|
85
|
+
**Important:** After adding or changing settings in `pretix.cfg`, you **must restart** the Pretix webserver and Celery workers for the changes to take effect.
|
86
|
+
|
87
|
+
Usage
|
88
|
+
-----
|
89
|
+
|
90
|
+
1. Once installed, configured, and enabled, the plugin works mostly automatically.
|
91
|
+
2. When an order is marked as paid, a background task is queued to geocode the address associated with the order (typically the invoice address). This requires your Celery worker to be running.
|
92
|
+
3. A "Sales Map" link will appear in the event control panel's sidebar navigation (usually near other order-related items or plugin links) for users with the "Can view orders" permission.
|
93
|
+
4. Clicking this link displays the map. You can toggle between the pin view (markers clustered) and the heatmap view using the button provided.
|
94
|
+
5. In the pin view:
|
95
|
+
|
96
|
+
* Hover over a marker cluster to see the number of orders it represents.
|
97
|
+
* Zoom in to break clusters apart.
|
98
|
+
* Hover over an individual pin to see a tooltip with Order Code, Date, and Item Count.
|
99
|
+
* Click an individual pin to open the corresponding order details page in a new tab.
|
100
|
+
|
101
|
+
Management Command: `geocode_existing_orders`
|
102
|
+
---------------------------------------------
|
103
|
+
|
104
|
+
This command is essential for processing orders that were placed *before* the map plugin was installed, enabled, or correctly configured with geocoding credentials. It scans paid orders and queues geocoding tasks for those that haven't been geocoded yet.
|
105
|
+
|
106
|
+
**When to Run:**
|
107
|
+
|
108
|
+
* After installing and configuring the plugin for the first time.
|
109
|
+
* If you previously ran the plugin without a working geocoding configuration or Celery worker.
|
110
|
+
* If you want to force-reprocess orders (e.g., if geocoding logic changed).
|
111
|
+
|
112
|
+
**Prerequisites:**
|
113
|
+
|
114
|
+
* Your Pretix Celery worker **must** be running to process the tasks queued by this command.
|
115
|
+
* Geocoding settings must be correctly configured in `pretix.cfg`.
|
116
|
+
|
117
|
+
**How to Run:**
|
118
|
+
|
119
|
+
1. Navigate to your Pretix installation directory (containing `manage.py`) in your server terminal.
|
120
|
+
2. Activate your Pretix virtual environment.
|
121
|
+
3. Execute the command using `manage.py`.
|
122
|
+
|
123
|
+
**Basic Command:**
|
124
|
+
|
125
|
+
.. code-block:: bash
|
126
|
+
|
127
|
+
python manage.py geocode_existing_orders [options]
|
128
|
+
|
129
|
+
**Available Options:**
|
130
|
+
|
131
|
+
* `--organizer <slug>`: Process orders only for the organizer with the given slug.
|
132
|
+
* Example: `python manage.py geocode_existing_orders --organizer=myorg`
|
133
|
+
* `--event <slug>`: Process orders only for the event with the given slug. **Requires** `--organizer` to be specified as well.
|
134
|
+
* Example: `python manage.py geocode_existing_orders --organizer=myorg --event=myevent2024`
|
135
|
+
* `--dry-run`: **Highly Recommended for first use!** Simulates the process and shows which orders *would* be queued, but doesn't actually queue any tasks. Use this to verify the scope and count before running for real.
|
136
|
+
* Example: `python manage.py geocode_existing_orders --dry-run`
|
137
|
+
* `--force-recode`: Queues geocoding tasks even for orders that already have an entry in the geocoding data table. Use this if you suspect previous geocoding attempts were incomplete or incorrect, or if the geocoding logic has been updated.
|
138
|
+
* Example: `python manage.py geocode_existing_orders --organizer=myorg --force-recode`
|
139
|
+
|
140
|
+
**Example Workflow:**
|
141
|
+
|
142
|
+
1. **Test with Dry Run (All Organizers):**
|
143
|
+
.. code-block:: bash
|
144
|
+
|
145
|
+
python manage.py geocode_existing_orders --dry-run
|
146
|
+
2. **(If satisfied) Run for Real (All Organizers):**
|
147
|
+
.. code-block:: bash
|
148
|
+
|
149
|
+
python manage.py geocode_existing_orders
|
150
|
+
3. **Monitor your Celery worker** logs to ensure tasks are being processed without errors.
|
151
|
+
|
152
|
+
|
153
|
+
Development setup
|
154
|
+
-----------------
|
155
|
+
|
156
|
+
1. Make sure that you have a working `pretix development setup`_. Ensure your dev setup includes a running Celery worker if you want to test the background tasks.
|
157
|
+
2. Clone this repository.
|
158
|
+
3. Activate the virtual environment you use for pretix development.
|
159
|
+
4. Execute ``python setup.py develop`` within this directory to register this application with pretix's plugin registry.
|
160
|
+
5. Execute ``make`` within this directory to compile translations.
|
161
|
+
6. **Configure Geocoding:** Add the necessary geocoding settings (e.g., `nominatim_user_agent`) to your local `pretix.cfg` file for testing the geocoding feature.
|
162
|
+
7. Restart your local pretix server and Celery worker. You can now use the plugin from this repository for your events by enabling it in the 'plugins' tab in the settings.
|
163
|
+
|
164
|
+
This plugin has CI set up to enforce a few code style rules. To check locally, you need these packages installed::
|
165
|
+
|
166
|
+
pip install flake8 isort black
|
167
|
+
|
168
|
+
To check your plugin for rule violations, run::
|
169
|
+
|
170
|
+
black --check .
|
171
|
+
isort -c .
|
172
|
+
flake8 .
|
173
|
+
|
174
|
+
You can auto-fix some of these issues by running::
|
175
|
+
|
176
|
+
isort .
|
177
|
+
black .
|
178
|
+
|
179
|
+
To automatically check for these issues before you commit, you can run ``.install-hooks``.
|
180
|
+
|
181
|
+
|
182
|
+
License
|
183
|
+
-------
|
184
|
+
|
185
|
+
Copyright 2025 MarkenJaden
|
186
|
+
|
187
|
+
Released under the terms of the Apache License 2.0
|
188
|
+
|
189
|
+
|
190
|
+
|
191
|
+
.. _pretix: https://github.com/pretix/pretix
|
192
|
+
.. _pretix installation: https://docs.pretix.eu/en/latest/administrator/installation/index.html
|
193
|
+
.. _pretix development setup: https://docs.pretix.eu/en/latest/development/setup.html
|
@@ -0,0 +1,45 @@
|
|
1
|
+
pretix_map-0.0.3.dist-info/licenses/LICENSE,sha256=RhQ89ePNDClBzEROahhwjDrBSEb5Zpx6XewZfGlY4Ss,569
|
2
|
+
pretix_mapplugin/__init__.py,sha256=4dCrvStS1OgE3Vlcv-qx5-TAo377yOMEToEqCi-PwY4,23
|
3
|
+
pretix_mapplugin/apps.py,sha256=AnThwyRw2AAz5f-kmXZ8hm85OmKnlDkRosVoQOBgPzE,830
|
4
|
+
pretix_mapplugin/geocoding.py,sha256=pZUF8pJ5mOFoNT1foRNuhCJO1XkJ8040AA9qIjrP8JI,4631
|
5
|
+
pretix_mapplugin/models.py,sha256=v0v9K0sb5OQHs5Gc6-jea_aEGECUQp1tZoYMwwb3YIM,994
|
6
|
+
pretix_mapplugin/signals.py,sha256=qUcM50bbHBET75ysI2C0UB7-969LjpLGRLeRLqm4sQo,3036
|
7
|
+
pretix_mapplugin/tasks.py,sha256=1y4IExpOWkepDVDaKOw9OgHUgLmPpmO6sN8Z8L-sB90,3590
|
8
|
+
pretix_mapplugin/urls.py,sha256=o5407vULF4S-bUihU7AeRxUcMyazg2lPjbvqRflsGxE,838
|
9
|
+
pretix_mapplugin/views.py,sha256=7WgmNZeqwmOesT6PrkAIRC8fNfAcWGm-j9-2YqF5egI,7146
|
10
|
+
pretix_mapplugin/locale/de/LC_MESSAGES/django.mo,sha256=6VVRAqa0ixL-lDA1QwoVvG0wd5ZBwYjaR4P8T73hxhU,269
|
11
|
+
pretix_mapplugin/locale/de/LC_MESSAGES/django.po,sha256=tIFKw9KOdGTjGq8bHV6tquRZe_MOn8TT4MJjdTRhId8,323
|
12
|
+
pretix_mapplugin/locale/de_Informal/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
|
+
pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.mo,sha256=6VVRAqa0ixL-lDA1QwoVvG0wd5ZBwYjaR4P8T73hxhU,269
|
14
|
+
pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.po,sha256=tIFKw9KOdGTjGq8bHV6tquRZe_MOn8TT4MJjdTRhId8,323
|
15
|
+
pretix_mapplugin/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
|
+
pretix_mapplugin/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
|
+
pretix_mapplugin/management/commands/geocode_existing_orders.py,sha256=pvGwC1j6NzSf2Y7r-Lk_-6c7YsgqLr-NCu0HT5VLaeY,8407
|
18
|
+
pretix_mapplugin/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
|
+
pretix_mapplugin/static/pretix_mapplugin/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
20
|
+
pretix_mapplugin/static/pretix_mapplugin/css/salesmap.css,sha256=z-OXFjpGWOoxv_tlYSDSUlcFLU9p03hhXI-8yxExl3k,598
|
21
|
+
pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js,sha256=zKSWlJp96VCt6PFVuw5xV8jA-yCyWDG1Vj94Vq7hQlA,16941
|
22
|
+
pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.Default.css,sha256=LWhzWaQGZRsWFrrJxg-6Zn8TT84k0_trtiHBc6qcGpY,1346
|
23
|
+
pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.css,sha256=-bdWuWOXMFkX0v9Cvr3OWClPiYefDQz9GGZP_7xZxdc,886
|
24
|
+
pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-heat.js,sha256=aPb_2lnWKnXsUc1_-aT9-kbtr4CV3c85jH9xC1e5QDI,5168
|
25
|
+
pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.esm.js,sha256=i7N_sDD-OoSdbNWYx4paYveaHkkprfXimr67-kGDy_M,430058
|
26
|
+
pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.esm.js.map,sha256=lxY34ye1PfsolFQ8pTsEtlgBJ4tW7panCBRsm35HNbs,866200
|
27
|
+
pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.js,sha256=Y0Ki5d8X0X1H2YiS590G4GgQTUnf6kP26s5XsJ2w76Q,455791
|
28
|
+
pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.js.map,sha256=l7cBxd_w6YZOXEWKixXC7DN4ejJsXed7kI-eoTvi1Wo,866292
|
29
|
+
pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.css,sha256=p4NxAoJBhIIN-hmNHrzRCf9tD_miZyoHS5obTRR9BMY,14806
|
30
|
+
pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.js,sha256=MQS1JlBNDWH9MJmkUh6H9zLMwxdN7FTgjea6i94-Ff8,147557
|
31
|
+
pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.js.map,sha256=YAoQ3FzREN4GmVENMir8vgHHypC0xfSK3CAxTHCqx1M,225544
|
32
|
+
pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js,sha256=WxJnsgS9OQGt16B_czxdDIl7Am9bwYkpj0eYwRE-Cy4,33726
|
33
|
+
pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js.map,sha256=R8juz3AGnE2aOl3MIx1ram0tNMMiec7EPYwDbqb2ycc,27636
|
34
|
+
pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/layers-2x.png,sha256=Bm2sqFDY_77wB68AsG6sABVyje4nnFHzy2xxbffELt8,1259
|
35
|
+
pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/layers.png,sha256=Hbvp0CjikvNvy6j4s6KNXokydU_CIVuaxp5M3s9RB8Y,696
|
36
|
+
pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-icon-2x.png,sha256=ABecTB7oMNOhCEEq4NKU9Vd2z-sIXGASmjmqb8SuJSg,2464
|
37
|
+
pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-icon.png,sha256=V0w6XMqF9BFAhbaEFZbWLwDXyJLHsD8oy_owHesdxDc,1466
|
38
|
+
pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-shadow.png,sha256=Jk9cZAM58ELdcpBiz8BMF_jqDymIK1OOOEjtjxDttNo,618
|
39
|
+
pretix_mapplugin/templates/pretix_mapplugin/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
40
|
+
pretix_mapplugin/templates/pretix_mapplugin/map_page.html,sha256=jUfPrCkwcbcTXgZ2d9a5wpUD1U7Y8g5rnB20hklKQ-k,2252
|
41
|
+
pretix_map-0.0.3.dist-info/METADATA,sha256=79Rqjxe9HVOOcz34UYPrC3vmNJEDtqRu1ck6CEyhI-o,9514
|
42
|
+
pretix_map-0.0.3.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
43
|
+
pretix_map-0.0.3.dist-info/entry_points.txt,sha256=C3NAjeZHoCekafkLMCJynPcABRTK8AUprtQv7sUNDZs,137
|
44
|
+
pretix_map-0.0.3.dist-info/top_level.txt,sha256=CAtEnkgA73zE9Gadm5mjt1SpXHBPOS-QWP0dQVoNToE,17
|
45
|
+
pretix_map-0.0.3.dist-info/RECORD,,
|
@@ -0,0 +1,15 @@
|
|
1
|
+
|
2
|
+
Copyright 2025 MarkenJaden
|
3
|
+
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
you may not use this file except in compliance with the License.
|
6
|
+
You may obtain a copy of the License at
|
7
|
+
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
See the License for the specific language governing permissions and
|
14
|
+
limitations under the License.
|
15
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
pretix_mapplugin
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "0.0.3"
|
pretix_mapplugin/apps.py
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
from django.utils.translation import gettext_lazy
|
2
|
+
|
3
|
+
from . import __version__
|
4
|
+
|
5
|
+
try:
|
6
|
+
from pretix.base.plugins import PluginConfig
|
7
|
+
except ImportError:
|
8
|
+
raise RuntimeError("Please use pretix 2.7 or above to run this plugin!")
|
9
|
+
|
10
|
+
|
11
|
+
class PluginApp(PluginConfig):
|
12
|
+
default = True
|
13
|
+
name = "pretix_mapplugin"
|
14
|
+
verbose_name = "Map-Plugin"
|
15
|
+
|
16
|
+
class PretixPluginMeta:
|
17
|
+
name = gettext_lazy("Map-Plugin")
|
18
|
+
author = "MarkenJaden"
|
19
|
+
description = gettext_lazy("An overview map of the catchment area of previous orders. Measured by postcode")
|
20
|
+
visible = True
|
21
|
+
version = __version__
|
22
|
+
category = "FEATURE"
|
23
|
+
compatibility = "pretix>=2.7.0"
|
24
|
+
settings_links = []
|
25
|
+
navigation_links = []
|
26
|
+
|
27
|
+
def ready(self):
|
28
|
+
from . import signals # NOQA
|
@@ -0,0 +1,113 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
# --- Import Django settings ---
|
4
|
+
from django.conf import settings
|
5
|
+
from geopy.exc import GeocoderServiceError, GeocoderTimedOut
|
6
|
+
from geopy.geocoders import Nominatim
|
7
|
+
from time import sleep
|
8
|
+
|
9
|
+
# Configure logging for your plugin
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
# --- Configuration Default ---
|
13
|
+
# Define a default/fallback User-Agent. Users *should* override this in pretix.cfg.
|
14
|
+
DEFAULT_NOMINATIM_USER_AGENT = "pretix-map-plugin/unknown (Please configure nominatim_user_agent in pretix.cfg)"
|
15
|
+
|
16
|
+
|
17
|
+
# --- Geocoding Function ---
|
18
|
+
|
19
|
+
def geocode_address(address_string: str) -> tuple[float, float] | None:
|
20
|
+
"""
|
21
|
+
Tries to geocode a given address string using Nominatim, reading the
|
22
|
+
User-Agent from Pretix configuration.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
address_string: A single string representing the address.
|
26
|
+
|
27
|
+
Returns:
|
28
|
+
A tuple (latitude, longitude) if successful, otherwise None.
|
29
|
+
"""
|
30
|
+
# --- Get User-Agent from Pretix Settings ---
|
31
|
+
# Access plugin settings via settings.plugins.<your_plugin_name>
|
32
|
+
# The .get() method allows providing a default value if the setting is missing.
|
33
|
+
user_agent = settings.plugins.pretix_mapplugin.get(
|
34
|
+
'nominatim_user_agent', # The setting name defined in pretix.cfg
|
35
|
+
DEFAULT_NOMINATIM_USER_AGENT
|
36
|
+
)
|
37
|
+
|
38
|
+
# Log a warning if the default User-Agent is being used, as it's required
|
39
|
+
# by Nominatim policy to be specific and include contact info.
|
40
|
+
if user_agent == DEFAULT_NOMINATIM_USER_AGENT:
|
41
|
+
logger.warning(
|
42
|
+
"Using default Nominatim User-Agent. Please set a specific "
|
43
|
+
"'nominatim_user_agent' under [pretix_mapplugin] in your "
|
44
|
+
"pretix.cfg according to Nominatim's usage policy."
|
45
|
+
)
|
46
|
+
# --- End Settings Retrieval ---
|
47
|
+
|
48
|
+
# Initialize the geolocator with the configured or default user_agent
|
49
|
+
geolocator = Nominatim(user_agent=user_agent)
|
50
|
+
|
51
|
+
try:
|
52
|
+
# Add a 1-second delay to respect Nominatim's usage policy (1 req/sec)
|
53
|
+
sleep(1)
|
54
|
+
|
55
|
+
location = geolocator.geocode(address_string, timeout=10)
|
56
|
+
|
57
|
+
if location:
|
58
|
+
logger.debug(
|
59
|
+
f"Geocoded '{address_string}' to ({location.latitude}, {location.longitude}) using User-Agent: {user_agent}")
|
60
|
+
return (location.latitude, location.longitude)
|
61
|
+
else:
|
62
|
+
logger.warning(f"Could not geocode address: {address_string} (Address not found by Nominatim)")
|
63
|
+
return None
|
64
|
+
|
65
|
+
except GeocoderTimedOut:
|
66
|
+
logger.error(f"Geocoding timed out for address: {address_string}")
|
67
|
+
return None
|
68
|
+
except GeocoderServiceError as e:
|
69
|
+
logger.error(f"Geocoding service error for address '{address_string}': {e}")
|
70
|
+
return None
|
71
|
+
except Exception as e:
|
72
|
+
logger.exception(f"An unexpected error occurred during geocoding for address '{address_string}': {e}")
|
73
|
+
return None
|
74
|
+
|
75
|
+
|
76
|
+
# --- Helper to Format Address from Pretix Order (No changes needed here) ---
|
77
|
+
|
78
|
+
def get_formatted_address_from_order(order) -> str | None:
|
79
|
+
"""
|
80
|
+
Creates a formatted address string from a Pretix order's invoice address.
|
81
|
+
"""
|
82
|
+
if not order.invoice_address:
|
83
|
+
return None
|
84
|
+
parts = []
|
85
|
+
if order.invoice_address.street: parts.append(order.invoice_address.street)
|
86
|
+
if order.invoice_address.city: parts.append(order.invoice_address.city)
|
87
|
+
if order.invoice_address.zipcode: parts.append(order.invoice_address.zipcode)
|
88
|
+
if order.invoice_address.state: parts.append(order.invoice_address.state)
|
89
|
+
if order.invoice_address.country: parts.append(str(order.invoice_address.country.name))
|
90
|
+
if not parts: return None
|
91
|
+
full_address = ", ".join(filter(None, parts))
|
92
|
+
return full_address
|
93
|
+
|
94
|
+
|
95
|
+
# --- Example Usage (Conceptual - No changes needed here) ---
|
96
|
+
# This function itself isn't called directly, the logic is in tasks.py
|
97
|
+
def process_order_for_geocoding(order):
|
98
|
+
"""Conceptual function showing how to use the geocoding."""
|
99
|
+
address_str = get_formatted_address_from_order(order)
|
100
|
+
if not address_str:
|
101
|
+
logger.info(f"Order {order.code} has no invoice address to geocode.")
|
102
|
+
return None
|
103
|
+
|
104
|
+
coordinates = geocode_address(address_str) # This now uses the configured User-Agent
|
105
|
+
|
106
|
+
if coordinates:
|
107
|
+
latitude, longitude = coordinates
|
108
|
+
logger.info(f"Successfully geocoded Order {order.code}: ({latitude}, {longitude})")
|
109
|
+
# Store coordinates...
|
110
|
+
return coordinates
|
111
|
+
else:
|
112
|
+
logger.warning(f"Failed to geocode Order {order.code} with address: {address_str}")
|
113
|
+
return None
|
Binary file
|
@@ -0,0 +1,12 @@
|
|
1
|
+
msgid ""
|
2
|
+
msgstr ""
|
3
|
+
"Project-Id-Version: \n"
|
4
|
+
"Report-Msgid-Bugs-To: \n"
|
5
|
+
"POT-Creation-Date: 2017-03-07 19:01+0100\n"
|
6
|
+
"PO-Revision-Date: \n"
|
7
|
+
"Last-Translator: MarkenJaden\n"
|
8
|
+
"Language-Team: \n"
|
9
|
+
"Language: de\n"
|
10
|
+
"MIME-Version: 1.0\n"
|
11
|
+
"Content-Type: text/plain; charset=UTF-8\n"
|
12
|
+
"Content-Transfer-Encoding: 8bit\n"
|
File without changes
|
Binary file
|
@@ -0,0 +1,12 @@
|
|
1
|
+
msgid ""
|
2
|
+
msgstr ""
|
3
|
+
"Project-Id-Version: \n"
|
4
|
+
"Report-Msgid-Bugs-To: \n"
|
5
|
+
"POT-Creation-Date: 2017-03-07 19:01+0100\n"
|
6
|
+
"PO-Revision-Date: \n"
|
7
|
+
"Last-Translator: MarkenJaden\n"
|
8
|
+
"Language-Team: \n"
|
9
|
+
"Language: de\n"
|
10
|
+
"MIME-Version: 1.0\n"
|
11
|
+
"Content-Type: text/plain; charset=UTF-8\n"
|
12
|
+
"Content-Transfer-Encoding: 8bit\n"
|
File without changes
|
File without changes
|
@@ -0,0 +1,167 @@
|
|
1
|
+
import logging
|
2
|
+
from django.core.exceptions import FieldDoesNotExist
|
3
|
+
from django.core.management.base import BaseCommand, CommandError
|
4
|
+
|
5
|
+
# Import scope activation helpers
|
6
|
+
from django_scopes import scope
|
7
|
+
|
8
|
+
# Import necessary Pretix models
|
9
|
+
from pretix.base.models import Event, Order, Organizer
|
10
|
+
|
11
|
+
# Import your Geocode model and the task
|
12
|
+
from pretix_mapplugin.models import OrderGeocodeData
|
13
|
+
from pretix_mapplugin.tasks import geocode_order_task
|
14
|
+
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
class Command(BaseCommand):
|
19
|
+
help = 'Scans paid orders and queues geocoding tasks for those missing geocode data.'
|
20
|
+
|
21
|
+
def add_arguments(self, parser):
|
22
|
+
parser.add_argument(
|
23
|
+
'--organizer',
|
24
|
+
type=str,
|
25
|
+
help='Slug of a specific organizer to process orders for.',
|
26
|
+
)
|
27
|
+
parser.add_argument(
|
28
|
+
'--event',
|
29
|
+
type=str,
|
30
|
+
help='Slug of a specific event to process orders for. Requires --organizer.',
|
31
|
+
)
|
32
|
+
parser.add_argument(
|
33
|
+
'--dry-run',
|
34
|
+
action='store_true',
|
35
|
+
help='Simulate the process without actually queuing tasks.',
|
36
|
+
)
|
37
|
+
parser.add_argument(
|
38
|
+
'--force-recode',
|
39
|
+
action='store_true',
|
40
|
+
help='Queue geocoding even for orders that already have geocode data.',
|
41
|
+
)
|
42
|
+
|
43
|
+
def handle(self, *args, **options):
|
44
|
+
organizer_slug = options['organizer']
|
45
|
+
event_slug = options['event']
|
46
|
+
dry_run = options['dry_run']
|
47
|
+
force_recode = options['force_recode']
|
48
|
+
|
49
|
+
if event_slug and not organizer_slug:
|
50
|
+
raise CommandError("You must specify --organizer when using --event.")
|
51
|
+
|
52
|
+
# --- Determine which organizers to process ---
|
53
|
+
organizers_to_process = []
|
54
|
+
if organizer_slug:
|
55
|
+
try:
|
56
|
+
# Fetch specific organizer (outside scope)
|
57
|
+
organizer = Organizer.objects.get(slug=organizer_slug)
|
58
|
+
organizers_to_process.append(organizer)
|
59
|
+
self.stdout.write(f"Processing specified organizer: {organizer.name} ({organizer_slug})")
|
60
|
+
except Organizer.DoesNotExist:
|
61
|
+
raise CommandError(f"Organizer with slug '{organizer_slug}' not found.")
|
62
|
+
else:
|
63
|
+
# Fetch all organizers (outside scope)
|
64
|
+
organizers_to_process = list(Organizer.objects.all())
|
65
|
+
self.stdout.write(f"Processing all {len(organizers_to_process)} organizers...")
|
66
|
+
|
67
|
+
# --- Initialize counters ---
|
68
|
+
total_queued = 0
|
69
|
+
total_skipped = 0
|
70
|
+
total_processed_orders = 0 # Track how many orders were checked
|
71
|
+
|
72
|
+
# --- Iterate through organizers and activate scope ---
|
73
|
+
for organizer in organizers_to_process:
|
74
|
+
self.stdout.write(f"\n--- Processing Organizer: {organizer.name} ({organizer.slug}) ---")
|
75
|
+
|
76
|
+
# --- Activate scope for this organizer ---
|
77
|
+
with scope(organizer=organizer):
|
78
|
+
# --- Now perform queries WITHIN the scope ---
|
79
|
+
|
80
|
+
# Start with paid orders FOR THIS ORGANIZER
|
81
|
+
orders_qs = Order.objects.filter(status=Order.STATUS_PAID)
|
82
|
+
|
83
|
+
# Filter by specific Event if requested
|
84
|
+
if event_slug and organizer.slug == organizer_slug: # Ensure we only filter for the specified org
|
85
|
+
try:
|
86
|
+
# Event query is now safe within organizer scope
|
87
|
+
event = Event.objects.get(slug=event_slug) # No need for organizer filter here
|
88
|
+
orders_qs = orders_qs.filter(event=event)
|
89
|
+
self.stdout.write(f" Filtering orders for event: {event.name} ({event_slug})")
|
90
|
+
except Event.DoesNotExist:
|
91
|
+
# Don't raise CommandError, just report and skip event for this organizer
|
92
|
+
self.stderr.write(self.style.WARNING(
|
93
|
+
f" Event '{event_slug}' not found for this organizer. Skipping event filter."))
|
94
|
+
# If only this event was requested for this organizer, skip to next organizer
|
95
|
+
if organizer_slug and event_slug:
|
96
|
+
continue
|
97
|
+
|
98
|
+
# Filter orders needing geocoding (within scope)
|
99
|
+
if not force_recode:
|
100
|
+
try:
|
101
|
+
# Check relation name - REPLACE 'geocode_data' if yours is different
|
102
|
+
relation_name = 'geocode_data' # Change if necessary
|
103
|
+
Order._meta.get_field(relation_name)
|
104
|
+
orders_to_process_qs = orders_qs.filter(**{f'{relation_name}__isnull': True})
|
105
|
+
self.stdout.write(" Selecting paid orders missing geocode data...")
|
106
|
+
except FieldDoesNotExist:
|
107
|
+
self.stderr.write(self.style.ERROR(
|
108
|
+
f" Could not find reverse relation '{relation_name}' on Order model. Check OrderGeocodeData model. Skipping organizer."))
|
109
|
+
continue # Skip this organizer if relation is wrong
|
110
|
+
except Exception as e:
|
111
|
+
self.stderr.write(
|
112
|
+
self.style.ERROR(f" Unexpected error checking relation: {e}. Skipping organizer."))
|
113
|
+
continue
|
114
|
+
else:
|
115
|
+
orders_to_process_qs = orders_qs
|
116
|
+
self.stdout.write(self.style.WARNING(
|
117
|
+
" Processing ALL selected paid orders for this organizer (--force-recode)..."))
|
118
|
+
|
119
|
+
# Get count within scope
|
120
|
+
current_org_orders_count = orders_to_process_qs.count()
|
121
|
+
total_processed_orders += orders_qs.count() # Count all checked orders for this org
|
122
|
+
|
123
|
+
if current_org_orders_count == 0:
|
124
|
+
self.stdout.write(" No orders need geocoding for this organizer/event.")
|
125
|
+
continue # Skip to next organizer
|
126
|
+
|
127
|
+
self.stdout.write(f" Found {current_org_orders_count} order(s) to potentially geocode.")
|
128
|
+
org_queued = 0
|
129
|
+
org_skipped = 0
|
130
|
+
|
131
|
+
# Iterate and queue (within scope)
|
132
|
+
for order in orders_to_process_qs.iterator():
|
133
|
+
if dry_run:
|
134
|
+
self.stdout.write(
|
135
|
+
f" [DRY RUN] Would queue Order: {order.code} (PK: {order.pk}) Event: {order.event.slug}")
|
136
|
+
org_queued += 1
|
137
|
+
else:
|
138
|
+
try:
|
139
|
+
geocode_order_task.apply_async(args=[order.pk])
|
140
|
+
# Be slightly less verbose inside the loop
|
141
|
+
# self.stdout.write(f" Queued Order: {order.code} (PK: {order.pk})")
|
142
|
+
org_queued += 1
|
143
|
+
except Exception as e:
|
144
|
+
self.stderr.write(self.style.ERROR(f" ERROR queuing Order {order.code}: {e}"))
|
145
|
+
logger.exception(f"Failed to queue geocoding task via command for order {order.code}: {e}")
|
146
|
+
org_skipped += 1
|
147
|
+
|
148
|
+
self.stdout.write(f" Queued: {org_queued}, Skipped: {org_skipped} for this organizer.")
|
149
|
+
total_queued += org_queued
|
150
|
+
total_skipped += org_skipped
|
151
|
+
|
152
|
+
# Scope for 'organizer' is automatically deactivated here by 'with' statement
|
153
|
+
|
154
|
+
# --- Final Report ---
|
155
|
+
self.stdout.write("=" * 40)
|
156
|
+
self.stdout.write("Overall Summary:")
|
157
|
+
self.stdout.write(f" Organizers processed: {len(organizers_to_process)}")
|
158
|
+
self.stdout.write(f" Total orders checked (paid): {total_processed_orders}") # Report total checked
|
159
|
+
if dry_run:
|
160
|
+
self.stdout.write(
|
161
|
+
self.style.SUCCESS(f"[DRY RUN] Complete. Would have queued tasks for {total_queued} order(s)."))
|
162
|
+
else:
|
163
|
+
self.stdout.write(self.style.SUCCESS(f"Complete. Queued tasks for {total_queued} order(s)."))
|
164
|
+
if total_skipped > 0:
|
165
|
+
self.stdout.write(
|
166
|
+
self.style.WARNING(f"Skipped {total_skipped} order(s) total due to errors during queueing."))
|
167
|
+
self.stdout.write("=" * 40)
|
File without changes
|
@@ -0,0 +1,27 @@
|
|
1
|
+
from django.db import models
|
2
|
+
from pretix.base.models import LoggedModel, Order
|
3
|
+
|
4
|
+
|
5
|
+
class OrderGeocodeData(LoggedModel):
|
6
|
+
"""
|
7
|
+
Stores the geocoded coordinates for a Pretix Order's invoice address.
|
8
|
+
"""
|
9
|
+
order = models.OneToOneField(
|
10
|
+
Order,
|
11
|
+
on_delete=models.CASCADE, # Delete geocode data if order is deleted
|
12
|
+
related_name='geocode_data', # Allows accessing this from order: order.geocode_data
|
13
|
+
primary_key=True # Use the order's PK as this model's PK for efficiency
|
14
|
+
)
|
15
|
+
latitude = models.FloatField()
|
16
|
+
longitude = models.FloatField()
|
17
|
+
geocoded_timestamp = models.DateTimeField(
|
18
|
+
auto_now_add=True # Automatically set when this record is created
|
19
|
+
)
|
20
|
+
|
21
|
+
class Meta:
|
22
|
+
# Optional: Define how instances are named in logs/admin
|
23
|
+
verbose_name = "Order Geocode Data"
|
24
|
+
verbose_name_plural = "Order Geocode Data"
|
25
|
+
|
26
|
+
def __str__(self):
|
27
|
+
return f"Geocode data for Order {self.order.code}"
|