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.
Files changed (45) hide show
  1. pretix_map-0.0.3.dist-info/METADATA +193 -0
  2. pretix_map-0.0.3.dist-info/RECORD +45 -0
  3. pretix_map-0.0.3.dist-info/WHEEL +5 -0
  4. pretix_map-0.0.3.dist-info/entry_points.txt +5 -0
  5. pretix_map-0.0.3.dist-info/licenses/LICENSE +15 -0
  6. pretix_map-0.0.3.dist-info/top_level.txt +1 -0
  7. pretix_mapplugin/__init__.py +1 -0
  8. pretix_mapplugin/apps.py +28 -0
  9. pretix_mapplugin/geocoding.py +113 -0
  10. pretix_mapplugin/locale/de/LC_MESSAGES/django.mo +0 -0
  11. pretix_mapplugin/locale/de/LC_MESSAGES/django.po +12 -0
  12. pretix_mapplugin/locale/de_Informal/.gitkeep +0 -0
  13. pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.mo +0 -0
  14. pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.po +12 -0
  15. pretix_mapplugin/management/__init__.py +0 -0
  16. pretix_mapplugin/management/commands/__init__.py +0 -0
  17. pretix_mapplugin/management/commands/geocode_existing_orders.py +167 -0
  18. pretix_mapplugin/migrations/__init__.py +0 -0
  19. pretix_mapplugin/models.py +27 -0
  20. pretix_mapplugin/signals.py +73 -0
  21. pretix_mapplugin/static/pretix_mapplugin/.gitkeep +0 -0
  22. pretix_mapplugin/static/pretix_mapplugin/css/salesmap.css +19 -0
  23. pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js +378 -0
  24. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.Default.css +60 -0
  25. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.css +14 -0
  26. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/layers-2x.png +0 -0
  27. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/layers.png +0 -0
  28. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-icon-2x.png +0 -0
  29. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-icon.png +0 -0
  30. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-shadow.png +0 -0
  31. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-heat.js +11 -0
  32. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.esm.js +14419 -0
  33. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.esm.js.map +1 -0
  34. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.js +14512 -0
  35. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.js.map +1 -0
  36. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.css +661 -0
  37. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.js +6 -0
  38. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.js.map +1 -0
  39. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js +3 -0
  40. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js.map +1 -0
  41. pretix_mapplugin/tasks.py +74 -0
  42. pretix_mapplugin/templates/pretix_mapplugin/.gitkeep +0 -0
  43. pretix_mapplugin/templates/pretix_mapplugin/map_page.html +45 -0
  44. pretix_mapplugin/urls.py +21 -0
  45. 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (78.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,5 @@
1
+ [distutils.commands]
2
+ build = pretix_plugin_build.build:CustomBuild
3
+
4
+ [pretix.plugin]
5
+ pretix_mapplugin = pretix_mapplugin:PretixPluginMeta
@@ -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"
@@ -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
@@ -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
@@ -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}"