pretix-map 0.1.1__py3-none-any.whl → 0.1.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.
@@ -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,10 +1,10 @@
1
- pretix_map-0.1.1.dist-info/licenses/LICENSE,sha256=RhQ89ePNDClBzEROahhwjDrBSEb5Zpx6XewZfGlY4Ss,569
2
- pretix_mapplugin/__init__.py,sha256=5xvN_gb61nKeq5TER5dSfcArTP3DVasZGN_MQq5dNpA,23
1
+ pretix_map-0.1.3.dist-info/licenses/LICENSE,sha256=RhQ89ePNDClBzEROahhwjDrBSEb5Zpx6XewZfGlY4Ss,569
2
+ pretix_mapplugin/__init__.py,sha256=LPNaKUFe5S0lxcbXP4IcviKz02tUsjRUDAJch2OV7IE,23
3
3
  pretix_mapplugin/apps.py,sha256=AnThwyRw2AAz5f-kmXZ8hm85OmKnlDkRosVoQOBgPzE,830
4
4
  pretix_mapplugin/geocoding.py,sha256=lBmwMvmE_cPyOHxWE8H3Se2P-2Eq0UjDTCv9gUs97Fo,4018
5
- pretix_mapplugin/models.py,sha256=v0v9K0sb5OQHs5Gc6-jea_aEGECUQp1tZoYMwwb3YIM,994
6
- pretix_mapplugin/signals.py,sha256=maBMMSq5M7diy_EaNgKr8KRfGFj0U437u-MEl2NVYBw,3661
7
- pretix_mapplugin/tasks.py,sha256=Blpleq0J3Sos7LDEXpH3EOkZcqE2y4VSiZDLyC2YWgU,4411
5
+ pretix_mapplugin/models.py,sha256=klKrgMu1bmiPBwucTdOAZGWtv4WAEKcnoeqPlZgFR1A,2091
6
+ pretix_mapplugin/signals.py,sha256=pSkucUPU6XgR0KLw4bKYEUW2Bqs5vfQXhO5YILQ2wps,3928
7
+ pretix_mapplugin/tasks.py,sha256=F_c36RwyTQzUJ9kBBos_-2zth1UXw_kpcQpUcE90BNM,5428
8
8
  pretix_mapplugin/urls.py,sha256=o5407vULF4S-bUihU7AeRxUcMyazg2lPjbvqRflsGxE,838
9
9
  pretix_mapplugin/views.py,sha256=7WgmNZeqwmOesT6PrkAIRC8fNfAcWGm-j9-2YqF5egI,7146
10
10
  pretix_mapplugin/locale/de/LC_MESSAGES/django.mo,sha256=6VVRAqa0ixL-lDA1QwoVvG0wd5ZBwYjaR4P8T73hxhU,269
@@ -14,12 +14,13 @@ pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.mo,sha256=6VVRAqa0ixL-lDA
14
14
  pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.po,sha256=tIFKw9KOdGTjGq8bHV6tquRZe_MOn8TT4MJjdTRhId8,323
15
15
  pretix_mapplugin/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  pretix_mapplugin/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- pretix_mapplugin/management/commands/geocode_existing_orders.py,sha256=QZGWXZfk-r3qffS3ernnOEsCK3feurc7hBpvCxhwa4U,9416
17
+ pretix_mapplugin/management/commands/geocode_existing_orders.py,sha256=Go9XzBb-eI_ohGDJ4x74MWIQkq2f1rMDLFNo0hp29AM,14127
18
18
  pretix_mapplugin/migrations/0001_initial.py,sha256=KAl1Egxptv1bpregGbsh8wUbr4Yh5A_zazVSAQdmoHM,1020
19
+ pretix_mapplugin/migrations/0002_remove_ordergeocodedata_geocoded_timestamp_and_more.py,sha256=dXmZRdqrND0pxiPRuitlDdg-Q2JBqYG-sRPJxr6Urpk,889
19
20
  pretix_mapplugin/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
21
  pretix_mapplugin/static/pretix_mapplugin/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- pretix_mapplugin/static/pretix_mapplugin/css/salesmap.css,sha256=z-OXFjpGWOoxv_tlYSDSUlcFLU9p03hhXI-8yxExl3k,598
22
- pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js,sha256=zKSWlJp96VCt6PFVuw5xV8jA-yCyWDG1Vj94Vq7hQlA,16941
22
+ pretix_mapplugin/static/pretix_mapplugin/css/salesmap.css,sha256=itRfmhct3UAz9rg0i8KYZDNQbN_sNgo8qoa1UPhvQW0,964
23
+ pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js,sha256=4SX0yr0VuJQro-A0Fnl6pTlAfk4X9XhjDxkGq95qUsQ,21272
23
24
  pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.Default.css,sha256=LWhzWaQGZRsWFrrJxg-6Zn8TT84k0_trtiHBc6qcGpY,1346
24
25
  pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.css,sha256=-bdWuWOXMFkX0v9Cvr3OWClPiYefDQz9GGZP_7xZxdc,886
25
26
  pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-heat.js,sha256=aPb_2lnWKnXsUc1_-aT9-kbtr4CV3c85jH9xC1e5QDI,5168
@@ -38,9 +39,9 @@ pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-ic
38
39
  pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-icon.png,sha256=V0w6XMqF9BFAhbaEFZbWLwDXyJLHsD8oy_owHesdxDc,1466
39
40
  pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-shadow.png,sha256=Jk9cZAM58ELdcpBiz8BMF_jqDymIK1OOOEjtjxDttNo,618
40
41
  pretix_mapplugin/templates/pretix_mapplugin/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
- pretix_mapplugin/templates/pretix_mapplugin/map_page.html,sha256=jUfPrCkwcbcTXgZ2d9a5wpUD1U7Y8g5rnB20hklKQ-k,2252
42
- pretix_map-0.1.1.dist-info/METADATA,sha256=az-2dh7UXVgqNYPr2ubPxeKnM2rBsRCNeyqb_V0pMiM,9518
43
- pretix_map-0.1.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
44
- pretix_map-0.1.1.dist-info/entry_points.txt,sha256=C3NAjeZHoCekafkLMCJynPcABRTK8AUprtQv7sUNDZs,137
45
- pretix_map-0.1.1.dist-info/top_level.txt,sha256=CAtEnkgA73zE9Gadm5mjt1SpXHBPOS-QWP0dQVoNToE,17
46
- pretix_map-0.1.1.dist-info/RECORD,,
42
+ pretix_mapplugin/templates/pretix_mapplugin/map_page.html,sha256=OC1CA5wAcDTC8BOejqO98u0vT0-pnfTgB1Frxk_3uWI,3391
43
+ pretix_map-0.1.3.dist-info/METADATA,sha256=LslAvtHnkh0p5WdCL6vJi5ERPpFNuOXzkvy7ZcRA-jQ,9518
44
+ pretix_map-0.1.3.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
45
+ pretix_map-0.1.3.dist-info/entry_points.txt,sha256=C3NAjeZHoCekafkLMCJynPcABRTK8AUprtQv7sUNDZs,137
46
+ pretix_map-0.1.3.dist-info/top_level.txt,sha256=CAtEnkgA73zE9Gadm5mjt1SpXHBPOS-QWP0dQVoNToE,17
47
+ pretix_map-0.1.3.dist-info/RECORD,,
@@ -1 +1 @@
1
- __version__ = "0.1.1"
1
+ __version__ = "0.1.3"
@@ -1,6 +1,8 @@
1
1
  import logging
2
+ import time # Import time for sleep
2
3
  from django.core.management.base import BaseCommand, CommandError
3
- from django.core.exceptions import FieldDoesNotExist
4
+ from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist
5
+ from django.db import transaction
4
6
 
5
7
  # --- Import Pretix Global Settings accessor ---
6
8
  from django_scopes import scope
@@ -21,22 +23,31 @@ except ImportError:
21
23
  except ImportError:
22
24
  # Fallback or raise error if neither is found
23
25
  logger.error("Could not determine Pretix settings accessor for management command.")
24
- raise ImportError("Cannot find Pretix settings accessor.")
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
25
31
 
26
32
  # --- Import necessary Pretix models ---
27
33
  from pretix.base.models import Order, Event, Organizer
28
34
 
29
- # --- Import your Geocode model and the task ---
35
+ # --- Import your Geocode model and geocoding functions ---
30
36
  from pretix_mapplugin.models import OrderGeocodeData
31
- from pretix_mapplugin.tasks import geocode_order_task
32
- # --- Import Default User-Agent ---
33
- from pretix_mapplugin.geocoding import DEFAULT_NOMINATIM_USER_AGENT
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
+ )
34
43
 
35
44
  logger = logging.getLogger(__name__)
36
45
 
37
46
 
38
47
  class Command(BaseCommand):
39
- help = 'Scans paid orders and queues geocoding tasks for those missing geocode data.'
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.')
40
51
 
41
52
  def add_arguments(self, parser):
42
53
  parser.add_argument(
@@ -46,11 +57,15 @@ class Command(BaseCommand):
46
57
  '--event', type=str, help='Slug of a specific event to process orders for. Requires --organizer.',
47
58
  )
48
59
  parser.add_argument(
49
- '--dry-run', action='store_true', help='Simulate the process without actually queuing tasks.',
60
+ '--dry-run', action='store_true', help='Simulate without geocoding or saving.',
50
61
  )
51
62
  parser.add_argument(
52
63
  '--force-recode', action='store_true',
53
- help='Queue geocoding even for orders that already have geocode data.',
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.'
54
69
  )
55
70
 
56
71
  def handle(self, *args, **options):
@@ -58,6 +73,14 @@ class Command(BaseCommand):
58
73
  event_slug = options['event']
59
74
  dry_run = options['dry_run']
60
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."))
61
84
 
62
85
  if event_slug and not organizer_slug:
63
86
  raise CommandError("You must specify --organizer when using --event.")
@@ -65,13 +88,10 @@ class Command(BaseCommand):
65
88
  # --- Read User-Agent using Pretix Settings accessor ---
66
89
  user_agent = DEFAULT_NOMINATIM_USER_AGENT
67
90
  try:
68
- # Get settings holder instance
69
91
  gs = SettingsProxy()
70
- # Construct the setting key specific to plugins
71
- # Format might be 'plugin:plugin_name:setting_name' or just 'plugin_name_setting_name'
72
- # Check Pretix docs or experiment if needed. Assuming the former.
73
92
  setting_key = 'plugin:pretix_mapplugin:nominatim_user_agent'
74
- user_agent = gs.settings.get(setting_key, DEFAULT_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)
75
95
 
76
96
  if user_agent == DEFAULT_NOMINATIM_USER_AGENT:
77
97
  self.stdout.write(self.style.WARNING(
@@ -79,9 +99,7 @@ class Command(BaseCommand):
79
99
  f"'{setting_key}' in your pretix.cfg."
80
100
  ))
81
101
  except Exception as e:
82
- # Catch broad exception during settings access
83
102
  self.stderr.write(self.style.ERROR(f"Failed to read plugin settings: {e}. Using default User-Agent."))
84
- # Continue with default user_agent
85
103
  # --- End Read User-Agent ---
86
104
 
87
105
  # --- Determine which organizers to process ---
@@ -98,36 +116,54 @@ class Command(BaseCommand):
98
116
  self.stdout.write(f"Processing all {len(organizers_to_process)} organizers...")
99
117
 
100
118
  # --- Initialize counters ---
101
- total_queued = 0
102
- total_skipped = 0
103
- total_processed_orders = 0
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
104
125
 
105
- # --- Iterate through organizers and activate scope ---
126
+ # --- Iterate through organizers ---
106
127
  for organizer in organizers_to_process:
107
128
  self.stdout.write(f"\n--- Processing Organizer: {organizer.name} ({organizer.slug}) ---")
129
+ # current_organizer_pk = organizer.pk # No longer needed for task
108
130
 
109
131
  with scope(organizer=organizer):
110
132
  # --- Get orders ---
111
- orders_qs = Order.objects.filter(status=Order.STATUS_PAID)
133
+ orders_qs = Order.objects.filter(status=Order.STATUS_PAID).select_related(
134
+ 'invoice_address', 'invoice_address__country', 'event'
135
+ )
112
136
 
113
- # --- Filter by event if specified ---
137
+ # --- Filter by event ---
114
138
  if event_slug and organizer.slug == organizer_slug:
115
139
  try:
116
140
  event = Event.objects.get(slug=event_slug)
117
141
  orders_qs = orders_qs.filter(event=event)
118
- self.stdout.write(f" Filtering orders for event: {event.name} ({event_slug})")
142
+ self.stdout.write(f" Filtering for event: {event.name} ({event_slug})")
119
143
  except Event.DoesNotExist:
120
- self.stderr.write(self.style.WARNING(
121
- f" Event '{event_slug}' not found for this organizer. Skipping event filter."))
144
+ self.stderr.write(
145
+ self.style.WARNING(f" Event '{event_slug}' not found. Skipping event filter."))
122
146
  if organizer_slug and event_slug: continue
123
147
 
124
- # --- Filter orders needing geocoding ---
125
- relation_name = 'geocode_data' # Ensure this matches your model
126
- if not force_recode:
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:
127
159
  try:
128
- Order._meta.get_field(relation_name) # Check existence
129
- orders_to_process_qs = orders_qs.filter(**{f'{relation_name}__isnull': True})
130
- self.stdout.write(" Selecting paid orders missing geocode data...")
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).")
131
167
  except FieldDoesNotExist:
132
168
  self.stderr.write(
133
169
  self.style.ERROR(f" Relation '{relation_name}' not found. Skipping organizer."))
@@ -135,58 +171,101 @@ class Command(BaseCommand):
135
171
  except Exception as e:
136
172
  self.stderr.write(self.style.ERROR(f" Error checking relation: {e}. Skipping organizer."))
137
173
  continue
138
- else:
139
- orders_to_process_qs = orders_qs
140
- self.stdout.write(self.style.WARNING(" Processing ALL selected paid orders (--force-recode)..."))
141
-
142
- # --- Process orders for this scope ---
143
- current_org_orders_count = orders_to_process_qs.count()
144
- all_checked_for_org = orders_qs.count()
145
- total_processed_orders += all_checked_for_org
146
174
 
147
- if current_org_orders_count == 0:
148
- self.stdout.write(f" No orders need geocoding ({all_checked_for_org} checked).")
175
+ if not orders_to_geocode_list:
176
+ self.stdout.write(" No orders require geocoding for this selection.")
149
177
  continue
150
178
 
151
- self.stdout.write(
152
- f" Found {current_org_orders_count} order(s) to potentially geocode ({all_checked_for_org} checked).")
153
- org_queued = 0
154
- org_skipped = 0
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
155
213
 
156
- for order in orders_to_process_qs.iterator():
157
214
  if dry_run:
158
- self.stdout.write(
159
- f" [DRY RUN] Would queue Order: {order.code} (PK: {order.pk}) Event: {order.event.slug}")
160
- org_queued += 1
215
+ self.stdout.write(self.style.SUCCESS(" [DRY RUN] Would geocode."))
216
+ org_geocoded += 1 # Simulate success for dry run count
161
217
  else:
218
+ # --- Perform Geocoding Directly ---
219
+ coordinates = geocode_address(address_str, nominatim_user_agent=user_agent)
220
+
221
+ # --- Save Result ---
162
222
  try:
163
- # --- Pass user_agent to task ---
164
- geocode_order_task.apply_async(
165
- args=[order.pk],
166
- kwargs={'nominatim_user_agent': user_agent} # Pass as kwarg
167
- )
168
- org_queued += 1
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
169
236
  except Exception as e:
170
- self.stderr.write(self.style.ERROR(f" ERROR queuing Order {order.code}: {e}"))
171
- logger.exception(f"Failed to queue geocoding task via command for order {order.code}: {e}")
172
- org_skipped += 1
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)
173
245
 
174
- self.stdout.write(f" Queued: {org_queued}, Skipped: {org_skipped} for this organizer.")
175
- total_queued += org_queued
176
- total_skipped += org_skipped
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}.")
177
253
  # End scope
178
254
 
179
- # --- Final Report ---
255
+ # --- Final Overall Report ---
180
256
  self.stdout.write("=" * 40)
181
- self.stdout.write("Overall Summary:")
257
+ self.stdout.write("Overall Geocoding Summary:")
182
258
  self.stdout.write(f" Organizers processed: {len(organizers_to_process)}")
183
- self.stdout.write(f" Total orders checked (paid): {total_processed_orders}")
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}")
184
261
  if dry_run:
185
- self.stdout.write(
186
- self.style.SUCCESS(f"[DRY RUN] Complete. Would have queued tasks for {total_queued} order(s)."))
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)."))
187
265
  else:
188
- self.stdout.write(self.style.SUCCESS(f"Complete. Queued tasks for {total_queued} order(s)."))
189
- if total_skipped > 0:
190
- self.stdout.write(
191
- self.style.WARNING(f"Skipped {total_skipped} order(s) total due to errors during queueing."))
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)"))
192
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
+ ]
@@ -2,26 +2,46 @@ from django.db import models
2
2
  from pretix.base.models import LoggedModel, Order
3
3
 
4
4
 
5
- class OrderGeocodeData(LoggedModel):
5
+ class OrderGeocodeData(LoggedModel): # Keep LoggedModel if you want audit logs
6
6
  """
7
7
  Stores the geocoded coordinates for a Pretix Order's invoice address.
8
+ Allows null coordinates for failed geocoding attempts.
8
9
  """
9
10
  order = models.OneToOneField(
10
11
  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
12
+ on_delete=models.CASCADE,
13
+ related_name='geocode_data',
14
+ primary_key=True # Keep this if you want order PK as primary key
14
15
  )
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
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."
19
29
  )
20
30
 
21
31
  class Meta:
22
- # Optional: Define how instances are named in logs/admin
23
32
  verbose_name = "Order Geocode Data"
24
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
+ # ]
25
38
 
26
39
  def __str__(self):
27
- return f"Geocode data for Order {self.order.code}"
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) ---
@@ -1,8 +1,36 @@
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
+
1
21
  #sales-map-container {
2
- height: 500px;
22
+ height: 100%;
3
23
  width: 100%;
4
- position: relative; /* Needed for Leaflet internal positioning */
5
- background: #eee; /* Optional: Light background while tiles load */
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%;
6
34
  }
7
35
 
8
36
  #map-status-overlay p {
@@ -12,8 +40,13 @@
12
40
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
13
41
  }
14
42
 
15
- #map-status-overlay p.text-danger { /* Style for error messages */
16
- color: #a94442; /* Bootstrap danger color */
17
- background-color: #f2dede; /* Bootstrap danger background */
18
- border-color: #ebccd1; /* Bootstrap danger border */
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;
19
52
  }