pretix-map 0.1.1__tar.gz → 0.1.3__tar.gz

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 (61) hide show
  1. {pretix_map-0.1.1/pretix_map.egg-info → pretix_map-0.1.3}/PKG-INFO +1 -1
  2. {pretix_map-0.1.1 → pretix_map-0.1.3/pretix_map.egg-info}/PKG-INFO +1 -1
  3. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_map.egg-info/SOURCES.txt +1 -0
  4. pretix_map-0.1.3/pretix_mapplugin/__init__.py +1 -0
  5. pretix_map-0.1.3/pretix_mapplugin/management/commands/geocode_existing_orders.py +271 -0
  6. pretix_map-0.1.3/pretix_mapplugin/migrations/0002_remove_ordergeocodedata_geocoded_timestamp_and_more.py +32 -0
  7. pretix_map-0.1.3/pretix_mapplugin/models.py +47 -0
  8. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/signals.py +27 -22
  9. pretix_map-0.1.3/pretix_mapplugin/static/pretix_mapplugin/css/salesmap.css +52 -0
  10. pretix_map-0.1.3/pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js +442 -0
  11. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/tasks.py +41 -18
  12. pretix_map-0.1.3/pretix_mapplugin/templates/pretix_mapplugin/map_page.html +68 -0
  13. pretix_map-0.1.1/pretix_mapplugin/__init__.py +0 -1
  14. pretix_map-0.1.1/pretix_mapplugin/management/commands/geocode_existing_orders.py +0 -192
  15. pretix_map-0.1.1/pretix_mapplugin/models.py +0 -27
  16. pretix_map-0.1.1/pretix_mapplugin/static/pretix_mapplugin/css/salesmap.css +0 -19
  17. pretix_map-0.1.1/pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js +0 -378
  18. pretix_map-0.1.1/pretix_mapplugin/templates/pretix_mapplugin/map_page.html +0 -45
  19. {pretix_map-0.1.1 → pretix_map-0.1.3}/LICENSE +0 -0
  20. {pretix_map-0.1.1 → pretix_map-0.1.3}/MANIFEST.in +0 -0
  21. {pretix_map-0.1.1 → pretix_map-0.1.3}/README.rst +0 -0
  22. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_map.egg-info/dependency_links.txt +0 -0
  23. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_map.egg-info/entry_points.txt +0 -0
  24. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_map.egg-info/requires.txt +0 -0
  25. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_map.egg-info/top_level.txt +0 -0
  26. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/apps.py +0 -0
  27. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/geocoding.py +0 -0
  28. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/locale/de/LC_MESSAGES/django.mo +0 -0
  29. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/locale/de/LC_MESSAGES/django.po +0 -0
  30. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/locale/de_Informal/.gitkeep +0 -0
  31. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.mo +0 -0
  32. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.po +0 -0
  33. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/management/__init__.py +0 -0
  34. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/management/commands/__init__.py +0 -0
  35. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/migrations/0001_initial.py +0 -0
  36. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/migrations/__init__.py +0 -0
  37. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/static/pretix_mapplugin/.gitkeep +0 -0
  38. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.Default.css +0 -0
  39. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.css +0 -0
  40. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/layers-2x.png +0 -0
  41. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/layers.png +0 -0
  42. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-icon-2x.png +0 -0
  43. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-icon.png +0 -0
  44. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-shadow.png +0 -0
  45. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-heat.js +0 -0
  46. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.esm.js +0 -0
  47. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.esm.js.map +0 -0
  48. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.js +0 -0
  49. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.js.map +0 -0
  50. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.css +0 -0
  51. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.js +0 -0
  52. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.js.map +0 -0
  53. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js +0 -0
  54. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js.map +0 -0
  55. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/templates/pretix_mapplugin/.gitkeep +0 -0
  56. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/urls.py +0 -0
  57. {pretix_map-0.1.1 → pretix_map-0.1.3}/pretix_mapplugin/views.py +0 -0
  58. {pretix_map-0.1.1 → pretix_map-0.1.3}/pyproject.toml +0 -0
  59. {pretix_map-0.1.1 → pretix_map-0.1.3}/setup.cfg +0 -0
  60. {pretix_map-0.1.1 → pretix_map-0.1.3}/setup.py +0 -0
  61. {pretix_map-0.1.1 → pretix_map-0.1.3}/tests/test_main.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pretix-map
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: An overview map of the catchment area of previous orders. Measured by postcode
5
5
  Author-email: MarkenJaden <jjsch1410@gmail.com>
6
6
  Maintainer-email: MarkenJaden <jjsch1410@gmail.com>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pretix-map
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: An overview map of the catchment area of previous orders. Measured by postcode
5
5
  Author-email: MarkenJaden <jjsch1410@gmail.com>
6
6
  Maintainer-email: MarkenJaden <jjsch1410@gmail.com>
@@ -27,6 +27,7 @@ pretix_mapplugin/management/__init__.py
27
27
  pretix_mapplugin/management/commands/__init__.py
28
28
  pretix_mapplugin/management/commands/geocode_existing_orders.py
29
29
  pretix_mapplugin/migrations/0001_initial.py
30
+ pretix_mapplugin/migrations/0002_remove_ordergeocodedata_geocoded_timestamp_and_more.py
30
31
  pretix_mapplugin/migrations/__init__.py
31
32
  pretix_mapplugin/static/pretix_mapplugin/.gitkeep
32
33
  pretix_mapplugin/static/pretix_mapplugin/css/salesmap.css
@@ -0,0 +1 @@
1
+ __version__ = "0.1.3"
@@ -0,0 +1,271 @@
1
+ import logging
2
+ import time # Import time for sleep
3
+ from django.core.management.base import BaseCommand, CommandError
4
+ from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist
5
+ from django.db import transaction
6
+
7
+ # --- Import Pretix Global Settings accessor ---
8
+ from django_scopes import scope
9
+
10
+ # Check if Pretix version uses AbstractSettingsHolder or GlobalSettingsObject
11
+ # Adjust import based on Pretix version if needed. Assume AbstractSettingsHolder for newer Pretix.
12
+ try:
13
+ from pretix.base.settings import GlobalSettingsObject as SettingsProxy
14
+ except ImportError:
15
+ try:
16
+ # Older pretix might use this pattern
17
+ from pretix.base.services.config import load_config
18
+
19
+
20
+ class SettingsProxy:
21
+ def __init__(self):
22
+ self.settings = load_config()
23
+ except ImportError:
24
+ # Fallback or raise error if neither is found
25
+ logger.error("Could not determine Pretix settings accessor for management command.")
26
+
27
+
28
+ # This will likely cause the command to fail later, but allows it to start
29
+ class SettingsProxy:
30
+ def __init__(self): self.settings = {} # Empty dict to avoid errors later
31
+
32
+ # --- Import necessary Pretix models ---
33
+ from pretix.base.models import Order, Event, Organizer
34
+
35
+ # --- Import your Geocode model and geocoding functions ---
36
+ from pretix_mapplugin.models import OrderGeocodeData
37
+ # --- Import geocoding functions directly, NOT the task ---
38
+ from pretix_mapplugin.geocoding import (
39
+ geocode_address,
40
+ get_formatted_address_from_order,
41
+ DEFAULT_NOMINATIM_USER_AGENT
42
+ )
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+
47
+ class Command(BaseCommand):
48
+ help = ('Scans paid orders and geocodes addresses for those missing geocode data directly '
49
+ 'within the command, respecting Nominatim rate limits (approx 1 req/sec). '
50
+ 'This can take a long time for many orders.')
51
+
52
+ def add_arguments(self, parser):
53
+ parser.add_argument(
54
+ '--organizer', type=str, help='Slug of a specific organizer to process orders for.',
55
+ )
56
+ parser.add_argument(
57
+ '--event', type=str, help='Slug of a specific event to process orders for. Requires --organizer.',
58
+ )
59
+ parser.add_argument(
60
+ '--dry-run', action='store_true', help='Simulate without geocoding or saving.',
61
+ )
62
+ parser.add_argument(
63
+ '--force-recode', action='store_true',
64
+ help='Geocode even for orders that already have geocode data.',
65
+ )
66
+ parser.add_argument(
67
+ '--delay', type=float, default=1.1,
68
+ help='Delay in seconds between geocoding requests (default: 1.1 to be safe). Set to 0 to disable.'
69
+ )
70
+
71
+ def handle(self, *args, **options):
72
+ organizer_slug = options['organizer']
73
+ event_slug = options['event']
74
+ dry_run = options['dry_run']
75
+ force_recode = options['force_recode']
76
+ delay = options['delay']
77
+
78
+ if delay < 1.0 and delay != 0: # Allow disabling delay with 0
79
+ self.stdout.write(self.style.WARNING(
80
+ f"Delay is {delay}s, which is less than 1 second. This may violate Nominatim usage policy."))
81
+ elif delay == 0:
82
+ self.stdout.write(
83
+ self.style.WARNING("Delay is disabled (--delay 0). Ensure you comply with geocoding service terms."))
84
+
85
+ if event_slug and not organizer_slug:
86
+ raise CommandError("You must specify --organizer when using --event.")
87
+
88
+ # --- Read User-Agent using Pretix Settings accessor ---
89
+ user_agent = DEFAULT_NOMINATIM_USER_AGENT
90
+ try:
91
+ gs = SettingsProxy()
92
+ setting_key = 'plugin:pretix_mapplugin:nominatim_user_agent'
93
+ # Use .get() which is safer for dictionaries possibly returned by load_config
94
+ user_agent = getattr(gs, 'settings', {}).get(setting_key, DEFAULT_NOMINATIM_USER_AGENT)
95
+
96
+ if user_agent == DEFAULT_NOMINATIM_USER_AGENT:
97
+ self.stdout.write(self.style.WARNING(
98
+ "Using default Nominatim User-Agent. Please set a specific "
99
+ f"'{setting_key}' in your pretix.cfg."
100
+ ))
101
+ except Exception as e:
102
+ self.stderr.write(self.style.ERROR(f"Failed to read plugin settings: {e}. Using default User-Agent."))
103
+ # --- End Read User-Agent ---
104
+
105
+ # --- Determine which organizers to process ---
106
+ organizers_to_process = []
107
+ if organizer_slug:
108
+ try:
109
+ organizer = Organizer.objects.get(slug=organizer_slug)
110
+ organizers_to_process.append(organizer)
111
+ self.stdout.write(f"Processing specified organizer: {organizer.name} ({organizer_slug})")
112
+ except Organizer.DoesNotExist:
113
+ raise CommandError(f"Organizer with slug '{organizer_slug}' not found.")
114
+ else:
115
+ organizers_to_process = list(Organizer.objects.all())
116
+ self.stdout.write(f"Processing all {len(organizers_to_process)} organizers...")
117
+
118
+ # --- Initialize counters ---
119
+ total_processed_count = 0 # Orders actually attempted (had address or forced)
120
+ total_geocoded_success = 0
121
+ total_geocode_failed = 0 # Geocoder returned None
122
+ total_skipped_no_address = 0
123
+ total_skipped_db_error = 0
124
+ total_checked_count = 0 # All orders matching initial filter
125
+
126
+ # --- Iterate through organizers ---
127
+ for organizer in organizers_to_process:
128
+ self.stdout.write(f"\n--- Processing Organizer: {organizer.name} ({organizer.slug}) ---")
129
+ # current_organizer_pk = organizer.pk # No longer needed for task
130
+
131
+ with scope(organizer=organizer):
132
+ # --- Get orders ---
133
+ orders_qs = Order.objects.filter(status=Order.STATUS_PAID).select_related(
134
+ 'invoice_address', 'invoice_address__country', 'event'
135
+ )
136
+
137
+ # --- Filter by event ---
138
+ if event_slug and organizer.slug == organizer_slug:
139
+ try:
140
+ event = Event.objects.get(slug=event_slug)
141
+ orders_qs = orders_qs.filter(event=event)
142
+ self.stdout.write(f" Filtering for event: {event.name} ({event_slug})")
143
+ except Event.DoesNotExist:
144
+ self.stderr.write(
145
+ self.style.WARNING(f" Event '{event_slug}' not found. Skipping event filter."))
146
+ if organizer_slug and event_slug: continue
147
+
148
+ # --- Determine which orders to process ---
149
+ relation_name = 'geocode_data' # Check model
150
+ orders_to_geocode_list = []
151
+ current_checked_count = orders_qs.count() # Count before filtering for geocode status
152
+ total_checked_count += current_checked_count
153
+
154
+ if force_recode:
155
+ orders_to_geocode_list = list(orders_qs)
156
+ self.stdout.write(self.style.WARNING(
157
+ f" Will process all {len(orders_to_geocode_list)} orders (--force-recode)..."))
158
+ else:
159
+ try:
160
+ Order._meta.get_field(relation_name)
161
+ existing_pks = set(OrderGeocodeData.objects.filter(
162
+ order__in=orders_qs
163
+ ).values_list('order_id', flat=True))
164
+ orders_to_geocode_list = list(orders_qs.exclude(pk__in=existing_pks))
165
+ self.stdout.write(
166
+ f" Found {len(orders_to_geocode_list)} orders missing geocode data (out of {current_checked_count} checked).")
167
+ except FieldDoesNotExist:
168
+ self.stderr.write(
169
+ self.style.ERROR(f" Relation '{relation_name}' not found. Skipping organizer."))
170
+ continue
171
+ except Exception as e:
172
+ self.stderr.write(self.style.ERROR(f" Error checking relation: {e}. Skipping organizer."))
173
+ continue
174
+
175
+ if not orders_to_geocode_list:
176
+ self.stdout.write(" No orders require geocoding for this selection.")
177
+ continue
178
+
179
+ # --- Process Orders Sequentially ---
180
+ count_this_org = 0
181
+ org_geocoded = 0
182
+ org_failed = 0
183
+ org_skipped_no_addr = 0
184
+ org_skipped_db = 0
185
+
186
+ for i, order in enumerate(orders_to_geocode_list):
187
+ count_this_org += 1
188
+ self.stdout.write(
189
+ f" Processing order {count_this_org}/{len(orders_to_geocode_list)}: {order.code} ...",
190
+ ending="")
191
+
192
+ address_str = get_formatted_address_from_order(order)
193
+ if not address_str:
194
+ self.stdout.write(self.style.WARNING(" No address. Skipping."))
195
+ total_skipped_no_address += 1
196
+ org_skipped_no_addr += 1
197
+ # Save null coords to prevent re-processing if not forcing
198
+ if not dry_run and not force_recode:
199
+ try:
200
+ with transaction.atomic():
201
+ OrderGeocodeData.objects.update_or_create(order=order, defaults={'latitude': None,
202
+ 'longitude': None})
203
+ except Exception as db_err:
204
+ self.stdout.write(self.style.ERROR(f" FAILED (DB Error saving null: {db_err})"))
205
+ logger.exception(
206
+ f"Failed to save null geocode data via command for order {order.code}: {db_err}")
207
+ total_skipped_db_error += 1
208
+ org_skipped_db += 1
209
+ continue # Move to next order
210
+
211
+ # Only increment this if we actually attempt geocoding
212
+ total_processed_count += 1
213
+
214
+ if dry_run:
215
+ self.stdout.write(self.style.SUCCESS(" [DRY RUN] Would geocode."))
216
+ org_geocoded += 1 # Simulate success for dry run count
217
+ else:
218
+ # --- Perform Geocoding Directly ---
219
+ coordinates = geocode_address(address_str, nominatim_user_agent=user_agent)
220
+
221
+ # --- Save Result ---
222
+ try:
223
+ with transaction.atomic():
224
+ obj, created = OrderGeocodeData.objects.update_or_create(
225
+ order=order,
226
+ defaults={'latitude': coordinates[0] if coordinates else None,
227
+ 'longitude': coordinates[1] if coordinates else None}
228
+ )
229
+ if coordinates:
230
+ self.stdout.write(
231
+ self.style.SUCCESS(f" OK ({coordinates[0]:.4f}, {coordinates[1]:.4f})"))
232
+ org_geocoded += 1
233
+ else:
234
+ self.stdout.write(self.style.WARNING(" FAILED (Geocode returned None)"))
235
+ org_failed += 1
236
+ except Exception as e:
237
+ self.stdout.write(self.style.ERROR(f" FAILED (DB Error: {e})"))
238
+ logger.exception(f"Failed to save geocode data via command for order {order.code}: {e}")
239
+ org_skipped_db += 1 # Count DB errors separately
240
+
241
+ # --- Apply Delay ---
242
+ if delay > 0 and i < len(
243
+ orders_to_geocode_list) - 1: # Don't sleep after the last one or if delay is 0
244
+ time.sleep(delay)
245
+
246
+ # Add org counts to totals
247
+ total_geocoded_success += org_geocoded
248
+ total_geocode_failed += org_failed
249
+ total_skipped_db_error += org_skipped_db
250
+
251
+ self.stdout.write(f" Finished Organizer: Succeeded: {org_geocoded}, Failed Geocode: {org_failed}, "
252
+ f"Skipped (No Addr): {org_skipped_no_addr}, Skipped (DB Err): {org_skipped_db}.")
253
+ # End scope
254
+
255
+ # --- Final Overall Report ---
256
+ self.stdout.write("=" * 40)
257
+ self.stdout.write("Overall Geocoding Summary:")
258
+ self.stdout.write(f" Organizers processed: {len(organizers_to_process)}")
259
+ self.stdout.write(f" Total Orders Checked (paid, matching filters): {total_checked_count}")
260
+ self.stdout.write(f" Total Orders Attempted (had address or forced): {total_processed_count}")
261
+ if dry_run:
262
+ self.stdout.write(self.style.SUCCESS(
263
+ f"[DRY RUN] Complete. Would have attempted geocoding for {total_processed_count} orders "
264
+ f"(+ {total_skipped_no_address} skipped due to no address)."))
265
+ else:
266
+ self.stdout.write(self.style.SUCCESS(f" Successfully Geocoded & Saved: {total_geocoded_success}"))
267
+ self.stdout.write(self.style.WARNING(f" Geocoding Failed (None returned): {total_geocode_failed}"))
268
+ self.stdout.write(f" Skipped (No Address): {total_skipped_no_address}")
269
+ if total_skipped_db_error > 0:
270
+ self.stdout.write(self.style.ERROR(f" Skipped (DB Save Error): {total_skipped_db_error} (check logs)"))
271
+ self.stdout.write("=" * 40)
@@ -0,0 +1,32 @@
1
+ # Generated by Django 4.2.20 on 2025-04-16 01:22
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('pretix_mapplugin', '0001_initial'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RemoveField(
14
+ model_name='ordergeocodedata',
15
+ name='geocoded_timestamp',
16
+ ),
17
+ migrations.AddField(
18
+ model_name='ordergeocodedata',
19
+ name='last_geocoded_at',
20
+ field=models.DateTimeField(auto_now=True),
21
+ ),
22
+ migrations.AlterField(
23
+ model_name='ordergeocodedata',
24
+ name='latitude',
25
+ field=models.FloatField(null=True),
26
+ ),
27
+ migrations.AlterField(
28
+ model_name='ordergeocodedata',
29
+ name='longitude',
30
+ field=models.FloatField(null=True),
31
+ ),
32
+ ]
@@ -0,0 +1,47 @@
1
+ from django.db import models
2
+ from pretix.base.models import LoggedModel, Order
3
+
4
+
5
+ class OrderGeocodeData(LoggedModel): # Keep LoggedModel if you want audit logs
6
+ """
7
+ Stores the geocoded coordinates for a Pretix Order's invoice address.
8
+ Allows null coordinates for failed geocoding attempts.
9
+ """
10
+ order = models.OneToOneField(
11
+ Order,
12
+ on_delete=models.CASCADE,
13
+ related_name='geocode_data',
14
+ primary_key=True # Keep this if you want order PK as primary key
15
+ )
16
+ latitude = models.FloatField(
17
+ null=True, # Allow NULL in the database if geocoding fails
18
+ blank=True # Allow blank in forms/admin (good practice with null=True)
19
+ )
20
+ longitude = models.FloatField(
21
+ null=True, # Allow NULL in the database if geocoding fails
22
+ blank=True # Allow blank in forms/admin
23
+ )
24
+
25
+ # Change to auto_now to update timestamp on every save (successful or null)
26
+ last_geocoded_at = models.DateTimeField(
27
+ auto_now=True, # Set/Update timestamp every time the record is saved
28
+ help_text="Timestamp when geocoding was last attempted/updated."
29
+ )
30
+
31
+ class Meta:
32
+ verbose_name = "Order Geocode Data"
33
+ verbose_name_plural = "Order Geocode Data"
34
+ # Optional: Indexing coordinates can speed up map data queries if you have many entries
35
+ # indexes = [
36
+ # models.Index(fields=['latitude', 'longitude']),
37
+ # ]
38
+
39
+ def __str__(self):
40
+ # Provide more informative string representation
41
+ if self.latitude is not None and self.longitude is not None:
42
+ return f"Geocode for Order {self.order.code}: ({self.latitude:.4f}, {self.longitude:.4f})"
43
+ else:
44
+ # Indicate if it's pending (never attempted) or failed (null coords stored)
45
+ # This requires knowing if the record exists but has nulls vs doesn't exist yet
46
+ # The current __str__ assumes the record exists if called.
47
+ return f"Geocode data for Order {self.order.code} (Coordinates: None)"
@@ -3,7 +3,6 @@ from django.dispatch import receiver
3
3
  from django.urls import reverse, NoReverseMatch
4
4
  from django.utils.translation import gettext_lazy as _
5
5
  from django.http import HttpRequest
6
- # Import Django settings to read config in web process
7
6
  from django.conf import settings
8
7
 
9
8
  # --- Pretix Signals ---
@@ -13,50 +12,56 @@ from pretix.control.signals import nav_event
13
12
  # --- Tasks ---
14
13
  from .tasks import geocode_order_task
15
14
  # --- Geocoding Default ---
16
- from .geocoding import DEFAULT_NOMINATIM_USER_AGENT # Import default
15
+ from .geocoding import DEFAULT_NOMINATIM_USER_AGENT
17
16
 
18
17
  logger = logging.getLogger(__name__)
19
18
 
20
19
  # --- Constants ---
21
20
  MAP_VIEW_URL_NAME = 'plugins:pretix_mapplugin:event.settings.salesmap.show'
22
21
  REQUIRED_MAP_PERMISSION = 'can_view_orders'
23
- PLUGIN_NAME = 'pretix_mapplugin' # Define plugin name for settings access
22
+ PLUGIN_NAME = 'pretix_mapplugin'
24
23
 
25
24
 
26
- # --- Signal Receiver for Geocoding (Reads setting, passes to task) ---
25
+ # --- Signal Receiver for Geocoding (Passes organizer_pk) ---
27
26
  @receiver(order_paid, dispatch_uid="sales_mapper_order_paid_geocode")
28
27
  def trigger_geocoding_on_payment(sender, order, **kwargs):
29
28
  """
30
29
  Listens for the order_paid signal, reads geocoding config,
31
- and queues the geocoding task with the config.
30
+ and queues the geocoding task with order_pk, organizer_pk, and config.
32
31
  """
33
- user_agent = DEFAULT_NOMINATIM_USER_AGENT # Start with default
32
+ user_agent = DEFAULT_NOMINATIM_USER_AGENT
33
+ organizer_pk = None # Initialize
34
34
  try:
35
- # --- Read User-Agent from settings (works in web process) ---
36
- # Check structure defensively before accessing
35
+ # Ensure order has event and organizer before proceeding
36
+ if not order or not order.event or not order.event.organizer:
37
+ logger.error(f"Order {order.code} is missing event or organizer information. Cannot queue task.")
38
+ return
39
+
40
+ organizer_pk = order.event.organizer.pk # Get organizer PK
41
+
42
+ # --- Read User-Agent from settings ---
37
43
  if hasattr(settings, 'plugins') and hasattr(settings.plugins, PLUGIN_NAME):
38
44
  plugin_settings = getattr(settings.plugins, PLUGIN_NAME)
39
- user_agent = plugin_settings.get(
40
- 'nominatim_user_agent', # Setting name in pretix.cfg
41
- DEFAULT_NOMINATIM_USER_AGENT
42
- )
45
+ user_agent = plugin_settings.get('nominatim_user_agent', DEFAULT_NOMINATIM_USER_AGENT)
43
46
  else:
44
- logger.warning(
45
- f"Could not access settings.plugins.{PLUGIN_NAME}, "
46
- "using default Nominatim User-Agent for task."
47
- )
47
+ logger.warning(f"Could not access settings.plugins.{PLUGIN_NAME}, using default User-Agent.")
48
48
 
49
- # --- Queue task with user_agent as keyword argument ---
49
+ # --- Queue task with user_agent and organizer_pk as keyword arguments ---
50
50
  geocode_order_task.apply_async(
51
- args=[order.pk],
52
- kwargs={'nominatim_user_agent': user_agent} # Pass as kwarg
51
+ args=[order.pk], # Keep order_pk as positional argument
52
+ kwargs={
53
+ 'nominatim_user_agent': user_agent,
54
+ 'organizer_pk': organizer_pk # Pass organizer PK
55
+ }
53
56
  )
54
- logger.info(f"Geocoding task queued for paid order {order.code} (PK: {order.pk}).")
57
+ logger.info(f"Geocoding task queued for paid order {order.code} (PK: {order.pk}, Org PK: {organizer_pk}).")
55
58
 
56
- except ImportError: # Error finding geocode_order_task itself if tasks.py fails
59
+ except ImportError:
57
60
  logger.exception("Could not import geocode_order_task. Check tasks.py.")
58
61
  except Exception as e:
59
- logger.exception(f"Failed to queue geocoding task for order {order.code}: {e}")
62
+ # Log the organizer PK as well if available
63
+ org_info = f" (Org PK: {organizer_pk})" if organizer_pk else ""
64
+ logger.exception(f"Failed to queue geocoding task for order {order.code}{org_info}: {e}")
60
65
 
61
66
 
62
67
  # --- Signal Receiver for Adding Navigation Item (No changes needed) ---
@@ -0,0 +1,52 @@
1
+ .plugin-map-content-wrapper {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: calc(100vh - 200px);
5
+ min-height: 450px;
6
+ }
7
+
8
+ .plugin-map-content-wrapper h1,
9
+ .plugin-map-content-wrapper .form-group {
10
+ flex-shrink: 0;
11
+ margin-bottom: 1em;
12
+ }
13
+
14
+ .map-wrapper {
15
+ flex-grow: 1;
16
+ position: relative;
17
+ min-height: 0;
18
+ border: 1px solid #ccc;
19
+ }
20
+
21
+ #sales-map-container {
22
+ height: 100%;
23
+ width: 100%;
24
+ display: block;
25
+ position: relative;
26
+ }
27
+
28
+ #map-status-overlay {
29
+ position: absolute;
30
+ top: 0;
31
+ left: 0;
32
+ width: 100%;
33
+ height: 100%;
34
+ }
35
+
36
+ #map-status-overlay p {
37
+ padding: 1em;
38
+ background: #fff;
39
+ border-radius: 5px;
40
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
41
+ }
42
+
43
+ #heatmap-options-panel input[type=range] {
44
+ width: 100%;
45
+ display: block;
46
+ }
47
+
48
+ #map-status-overlay p.text-danger {
49
+ color: #a94442;
50
+ background-color: #f2dede;
51
+ border-color: #ebccd1;
52
+ }