pretix-map 0.1.2__py3-none-any.whl → 0.1.4__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.2
3
+ Version: 0.1.4
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,8 +1,8 @@
1
- pretix_map-0.1.2.dist-info/licenses/LICENSE,sha256=RhQ89ePNDClBzEROahhwjDrBSEb5Zpx6XewZfGlY4Ss,569
2
- pretix_mapplugin/__init__.py,sha256=O1y2i1C4m602NbHgLdHmpflu9TGf21O6bs3ZEn_x0w4,23
1
+ pretix_map-0.1.4.dist-info/licenses/LICENSE,sha256=RhQ89ePNDClBzEROahhwjDrBSEb5Zpx6XewZfGlY4Ss,569
2
+ pretix_mapplugin/__init__.py,sha256=wQL21SKqJmZ-NSboyC8tgIR3EmyN1XDjwrN7CAHZpGQ,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
5
+ pretix_mapplugin/models.py,sha256=klKrgMu1bmiPBwucTdOAZGWtv4WAEKcnoeqPlZgFR1A,2091
6
6
  pretix_mapplugin/signals.py,sha256=pSkucUPU6XgR0KLw4bKYEUW2Bqs5vfQXhO5YILQ2wps,3928
7
7
  pretix_mapplugin/tasks.py,sha256=F_c36RwyTQzUJ9kBBos_-2zth1UXw_kpcQpUcE90BNM,5428
8
8
  pretix_mapplugin/urls.py,sha256=o5407vULF4S-bUihU7AeRxUcMyazg2lPjbvqRflsGxE,838
@@ -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=y6v7Oug0kw6cLAn0iJgaFGv6NYGP9f9K-zNavlyJq_8,11703
17
+ pretix_mapplugin/management/commands/geocode_existing_orders.py,sha256=zD8OD7c1ZXPCR1KNOc-gWY8ZlIk_IbMKtagFinG6qf0,14099
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=9t2grYB2nWk90Q8h7XjDrlMw9UvcwYS4lcXM1KFidqI,964
23
+ pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js,sha256=ObXC0E0UOJMIiS5DobuGkTVjNzxDjODs8WgxbKaryWE,18591
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.2.dist-info/METADATA,sha256=Qf2yvDEHWVnIs5rZNPeHDTLDD-LLyWuoUKhVtg5yj9c,9518
43
- pretix_map-0.1.2.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
44
- pretix_map-0.1.2.dist-info/entry_points.txt,sha256=C3NAjeZHoCekafkLMCJynPcABRTK8AUprtQv7sUNDZs,137
45
- pretix_map-0.1.2.dist-info/top_level.txt,sha256=CAtEnkgA73zE9Gadm5mjt1SpXHBPOS-QWP0dQVoNToE,17
46
- pretix_map-0.1.2.dist-info/RECORD,,
42
+ pretix_mapplugin/templates/pretix_mapplugin/map_page.html,sha256=wrUFxvtlmBhgADoRqB7sL3QsGETNC_DgYjWboHvDYFw,4827
43
+ pretix_map-0.1.4.dist-info/METADATA,sha256=BkU4P9Kyz6cXKpmquqrTwkVXTd3ng0ygN_kmbP3YBuU,9518
44
+ pretix_map-0.1.4.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
45
+ pretix_map-0.1.4.dist-info/entry_points.txt,sha256=C3NAjeZHoCekafkLMCJynPcABRTK8AUprtQv7sUNDZs,137
46
+ pretix_map-0.1.4.dist-info/top_level.txt,sha256=CAtEnkgA73zE9Gadm5mjt1SpXHBPOS-QWP0dQVoNToE,17
47
+ pretix_map-0.1.4.dist-info/RECORD,,
@@ -1 +1 @@
1
- __version__ = "0.1.2"
1
+ __version__ = "0.1.4"
@@ -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
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
@@ -8,7 +10,6 @@ from django_scopes import scope
8
10
  # Check if Pretix version uses AbstractSettingsHolder or GlobalSettingsObject
9
11
  # Adjust import based on Pretix version if needed. Assume AbstractSettingsHolder for newer Pretix.
10
12
  try:
11
- # Newer Pretix often uses this pattern via AbstractSettingsHolder
12
13
  from pretix.base.settings import GlobalSettingsObject as SettingsProxy
13
14
  except ImportError:
14
15
  try:
@@ -31,38 +32,40 @@ except ImportError:
31
32
  # --- Import necessary Pretix models ---
32
33
  from pretix.base.models import Order, Event, Organizer
33
34
 
34
- # --- Import your Geocode model and the task ---
35
+ # --- Import your Geocode model and geocoding functions ---
35
36
  from pretix_mapplugin.models import OrderGeocodeData
36
- from pretix_mapplugin.tasks import geocode_order_task
37
- # --- Import Default User-Agent ---
38
- 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
+ )
39
43
 
40
44
  logger = logging.getLogger(__name__)
41
45
 
42
46
 
43
47
  class Command(BaseCommand):
44
- 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.')
45
51
 
46
52
  def add_arguments(self, parser):
47
53
  parser.add_argument(
48
- '--organizer',
49
- type=str,
50
- help='Slug of a specific organizer to process orders for.',
54
+ '--organizer', type=str, help='Slug of a specific organizer to process orders for.',
51
55
  )
52
56
  parser.add_argument(
53
- '--event',
54
- type=str,
55
- help='Slug of a specific event to process orders for. Requires --organizer.',
57
+ '--event', type=str, help='Slug of a specific event to process orders for. Requires --organizer.',
56
58
  )
57
59
  parser.add_argument(
58
- '--dry-run',
59
- action='store_true',
60
- help='Simulate the process without actually queuing tasks.',
60
+ '--dry-run', action='store_true', help='Simulate without geocoding or saving.',
61
61
  )
62
62
  parser.add_argument(
63
- '--force-recode',
64
- action='store_true',
65
- help='Queue geocoding even for orders that already have geocode data.',
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.'
66
69
  )
67
70
 
68
71
  def handle(self, *args, **options):
@@ -70,6 +73,14 @@ class Command(BaseCommand):
70
73
  event_slug = options['event']
71
74
  dry_run = options['dry_run']
72
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."))
73
84
 
74
85
  if event_slug and not organizer_slug:
75
86
  raise CommandError("You must specify --organizer when using --event.")
@@ -78,8 +89,6 @@ class Command(BaseCommand):
78
89
  user_agent = DEFAULT_NOMINATIM_USER_AGENT
79
90
  try:
80
91
  gs = SettingsProxy()
81
- # Construct the setting key specific to plugins
82
- # The format 'plugin:plugin_name:setting_name' is common
83
92
  setting_key = 'plugin:pretix_mapplugin:nominatim_user_agent'
84
93
  # Use .get() which is safer for dictionaries possibly returned by load_config
85
94
  user_agent = getattr(gs, 'settings', {}).get(setting_key, DEFAULT_NOMINATIM_USER_AGENT)
@@ -90,136 +99,173 @@ class Command(BaseCommand):
90
99
  f"'{setting_key}' in your pretix.cfg."
91
100
  ))
92
101
  except Exception as e:
93
- # Catch broad exception during settings access
94
102
  self.stderr.write(self.style.ERROR(f"Failed to read plugin settings: {e}. Using default User-Agent."))
95
- # Continue with default user_agent
96
103
  # --- End Read User-Agent ---
97
104
 
98
105
  # --- Determine which organizers to process ---
99
106
  organizers_to_process = []
100
107
  if organizer_slug:
101
108
  try:
102
- # Fetch specific organizer (outside scope)
103
109
  organizer = Organizer.objects.get(slug=organizer_slug)
104
110
  organizers_to_process.append(organizer)
105
111
  self.stdout.write(f"Processing specified organizer: {organizer.name} ({organizer_slug})")
106
112
  except Organizer.DoesNotExist:
107
113
  raise CommandError(f"Organizer with slug '{organizer_slug}' not found.")
108
114
  else:
109
- # Fetch all organizers (outside scope)
110
115
  organizers_to_process = list(Organizer.objects.all())
111
116
  self.stdout.write(f"Processing all {len(organizers_to_process)} organizers...")
112
117
 
113
118
  # --- Initialize counters ---
114
- total_queued = 0
115
- total_skipped = 0
116
- 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
117
125
 
118
- # --- Iterate through organizers and activate scope ---
126
+ # --- Iterate through organizers ---
119
127
  for organizer in organizers_to_process:
120
128
  self.stdout.write(f"\n--- Processing Organizer: {organizer.name} ({organizer.slug}) ---")
121
- current_organizer_pk = organizer.pk # Get the PK needed for the task kwarg
129
+ # current_organizer_pk = organizer.pk # No longer needed for task
122
130
 
123
131
  with scope(organizer=organizer):
124
- # --- Get orders within scope ---
125
- orders_qs = Order.objects.filter(status=Order.STATUS_PAID)
132
+ # --- Get orders ---
133
+ orders_qs = Order.objects.filter(status=Order.STATUS_PAID).select_related(
134
+ 'invoice_address', 'event'
135
+ )
126
136
 
127
- # --- Filter by event if specified ---
137
+ # --- Filter by event ---
128
138
  if event_slug and organizer.slug == organizer_slug:
129
139
  try:
130
140
  event = Event.objects.get(slug=event_slug)
131
141
  orders_qs = orders_qs.filter(event=event)
132
- self.stdout.write(f" Filtering orders for event: {event.name} ({event_slug})")
142
+ self.stdout.write(f" Filtering for event: {event.name} ({event_slug})")
133
143
  except Event.DoesNotExist:
134
- self.stderr.write(self.style.WARNING(
135
- f" Event '{event_slug}' not found for this organizer. Skipping event filter."))
136
- # If filtering by event and it's not found for this org, skip this org entirely
137
- if organizer_slug and event_slug:
138
- self.stdout.write(
139
- f" Skipping organizer '{organizer.slug}' as specified event was not found.")
140
- continue # Move to the next organizer
141
-
142
- # --- Filter orders needing geocoding ---
143
- relation_name = 'geocode_data' # Ensure this matches your model's related_name
144
- if not force_recode:
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:
145
159
  try:
146
- Order._meta.get_field(relation_name) # Check existence
147
- # Filter orders that don't have the related geocode entry
148
- orders_to_process_qs = orders_qs.filter(**{f'{relation_name}__isnull': True})
149
- 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).")
150
167
  except FieldDoesNotExist:
151
- # This indicates a code/model setup error, stop for this org
152
- self.stderr.write(self.style.ERROR(
153
- f" Configuration Error: Reverse relation '{relation_name}' not found on Order model. Check OrderGeocodeData model definition (related_name). Skipping organizer '{organizer.slug}'."))
154
- continue # Skip this organizer
168
+ self.stderr.write(
169
+ self.style.ERROR(f" Relation '{relation_name}' not found. Skipping organizer."))
170
+ continue
155
171
  except Exception as e:
156
- # Catch other potential errors during query construction
157
- self.stderr.write(self.style.ERROR(
158
- f" Error checking relation '{relation_name}': {e}. Skipping organizer '{organizer.slug}'."))
159
- continue # Skip this organizer
160
- else:
161
- # If forcing, process all orders that matched the initial filters (paid, event)
162
- orders_to_process_qs = orders_qs
163
- self.stdout.write(
164
- self.style.WARNING(" Processing ALL selected paid orders (--force-recode specified)..."))
172
+ self.stderr.write(self.style.ERROR(f" Error checking relation: {e}. Skipping organizer."))
173
+ continue
165
174
 
166
- # --- Process orders for this scope ---
167
- current_org_orders_count = orders_to_process_qs.count() # Count orders to queue
168
- all_checked_for_org = orders_qs.count() # Count all orders checked for this filter set
169
- total_processed_orders += all_checked_for_org # Add to overall total
175
+ if not orders_to_geocode_list:
176
+ self.stdout.write(" No orders require geocoding for this selection.")
177
+ continue
170
178
 
171
- if current_org_orders_count == 0:
172
- self.stdout.write(f" No orders need geocoding ({all_checked_for_org} checked).")
173
- continue # Skip to next organizer
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
174
185
 
175
- self.stdout.write(
176
- f" Found {current_org_orders_count} order(s) to potentially geocode ({all_checked_for_org} checked).")
177
- org_queued = 0
178
- org_skipped = 0
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
179
213
 
180
- # Iterate and queue tasks
181
- for order in orders_to_process_qs.iterator(): # Use iterator for memory efficiency
182
214
  if dry_run:
183
- # Provide slightly more info in dry run
184
- self.stdout.write(
185
- f" [DRY RUN] Would queue Order: {order.code} (PK: {order.pk}, Org PK: {current_organizer_pk}) Event: {order.event.slug}")
186
- org_queued += 1
215
+ self.stdout.write(self.style.SUCCESS(" [DRY RUN] Would geocode."))
216
+ org_geocoded += 1 # Simulate success for dry run count
187
217
  else:
218
+ # --- Perform Geocoding Directly ---
219
+ coordinates = geocode_address(address_str, nominatim_user_agent=user_agent)
220
+
221
+ # --- Save Result ---
188
222
  try:
189
- # --- Pass user_agent AND organizer_pk to the task ---
190
- geocode_order_task.apply_async(
191
- args=[order.pk], # Positional arg is order_pk
192
- kwargs={
193
- 'nominatim_user_agent': user_agent,
194
- 'organizer_pk': current_organizer_pk # Pass organizer PK
195
- }
196
- )
197
- # Don't log every single queue success unless verbose requested
198
- 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
199
236
  except Exception as e:
200
- # Log error if queueing fails
201
- self.stderr.write(self.style.ERROR(
202
- f" ERROR queuing task for Order {order.code} (PK: {order.pk}): {e}"))
203
- logger.exception(f"Failed to queue geocoding task via command for order {order.code}: {e}")
204
- org_skipped += 1
205
-
206
- # Report summary for the current organizer
207
- self.stdout.write(f" Finished Organizer: Queued: {org_queued}, Skipped: {org_skipped}.")
208
- total_queued += org_queued
209
- total_skipped += org_skipped
210
- # End scope 'with' block
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
211
254
 
212
255
  # --- Final Overall Report ---
213
256
  self.stdout.write("=" * 40)
214
- self.stdout.write("Overall Summary:")
257
+ self.stdout.write("Overall Geocoding Summary:")
215
258
  self.stdout.write(f" Organizers processed: {len(organizers_to_process)}")
216
- self.stdout.write(f" Total orders checked (paid, matching filters): {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}")
217
261
  if dry_run:
218
- self.stdout.write(
219
- 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)."))
220
265
  else:
221
- self.stdout.write(self.style.SUCCESS(f"Complete. Successfully queued tasks for {total_queued} order(s)."))
222
- if total_skipped > 0:
223
- self.stdout.write(self.style.WARNING(
224
- f"Skipped {total_skipped} order(s) total due to errors during queueing (check logs)."))
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)"))
225
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)"
@@ -1,8 +1,36 @@
1
+ .plugin-map-content-wrapper {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: calc(100vh - 100px);
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
  }