pretix-map 0.1.4__py3-none-any.whl → 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pretix_map-0.1.5.dist-info/METADATA +88 -0
- {pretix_map-0.1.4.dist-info → pretix_map-0.1.5.dist-info}/RECORD +32 -30
- {pretix_map-0.1.4.dist-info → pretix_map-0.1.5.dist-info}/WHEEL +1 -1
- {pretix_map-0.1.4.dist-info → pretix_map-0.1.5.dist-info}/licenses/LICENSE +15 -15
- pretix_mapplugin/__init__.py +1 -1
- pretix_mapplugin/apps.py +28 -28
- pretix_mapplugin/geocoding.py +162 -102
- pretix_mapplugin/locale/de/LC_MESSAGES/django.po +12 -12
- pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.po +12 -12
- pretix_mapplugin/management/commands/geocode_existing_orders.py +271 -271
- pretix_mapplugin/migrations/0001_initial.py +27 -27
- pretix_mapplugin/migrations/0002_remove_ordergeocodedata_geocoded_timestamp_and_more.py +32 -32
- pretix_mapplugin/migrations/0003_mapmilestone.py +27 -0
- pretix_mapplugin/models.py +71 -47
- pretix_mapplugin/signals.py +77 -92
- pretix_mapplugin/static/pretix_mapplugin/css/salesmap.css +51 -51
- pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js +342 -452
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.Default.css +59 -59
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.css +14 -14
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-heat.js +10 -10
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.esm.js +14419 -14419
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.js +14512 -14512
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.css +661 -661
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.js +5 -5
- pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js +2 -2
- pretix_mapplugin/tasks.py +144 -113
- pretix_mapplugin/templates/pretix_mapplugin/map_page.html +154 -88
- pretix_mapplugin/templates/pretix_mapplugin/milestones.html +53 -0
- pretix_mapplugin/urls.py +38 -21
- pretix_mapplugin/views.py +272 -163
- pretix_map-0.1.4.dist-info/METADATA +0 -195
- {pretix_map-0.1.4.dist-info → pretix_map-0.1.5.dist-info}/entry_points.txt +0 -0
- {pretix_map-0.1.4.dist-info → pretix_map-0.1.5.dist-info}/top_level.txt +0 -0
|
@@ -1,271 +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', '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)
|
|
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', '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)
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
# Generated by Django 4.2.20 on 2025-04-15 23:21
|
|
2
|
-
|
|
3
|
-
from django.db import migrations, models
|
|
4
|
-
import django.db.models.deletion
|
|
5
|
-
import pretix.base.models.base
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class Migration(migrations.Migration):
|
|
9
|
-
initial = True
|
|
10
|
-
|
|
11
|
-
operations = [
|
|
12
|
-
migrations.CreateModel(
|
|
13
|
-
name='OrderGeocodeData',
|
|
14
|
-
fields=[
|
|
15
|
-
('order', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True,
|
|
16
|
-
related_name='geocode_data', serialize=False, to='pretixbase.order')),
|
|
17
|
-
('latitude', models.FloatField()),
|
|
18
|
-
('longitude', models.FloatField()),
|
|
19
|
-
('geocoded_timestamp', models.DateTimeField(auto_now_add=True)),
|
|
20
|
-
],
|
|
21
|
-
options={
|
|
22
|
-
'verbose_name': 'Order Geocode Data',
|
|
23
|
-
'verbose_name_plural': 'Order Geocode Data',
|
|
24
|
-
},
|
|
25
|
-
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
|
26
|
-
),
|
|
27
|
-
]
|
|
1
|
+
# Generated by Django 4.2.20 on 2025-04-15 23:21
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
import django.db.models.deletion
|
|
5
|
+
import pretix.base.models.base
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
initial = True
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.CreateModel(
|
|
13
|
+
name='OrderGeocodeData',
|
|
14
|
+
fields=[
|
|
15
|
+
('order', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True,
|
|
16
|
+
related_name='geocode_data', serialize=False, to='pretixbase.order')),
|
|
17
|
+
('latitude', models.FloatField()),
|
|
18
|
+
('longitude', models.FloatField()),
|
|
19
|
+
('geocoded_timestamp', models.DateTimeField(auto_now_add=True)),
|
|
20
|
+
],
|
|
21
|
+
options={
|
|
22
|
+
'verbose_name': 'Order Geocode Data',
|
|
23
|
+
'verbose_name_plural': 'Order Geocode Data',
|
|
24
|
+
},
|
|
25
|
+
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
|
26
|
+
),
|
|
27
|
+
]
|