pretix-map 0.1.2__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.
- {pretix_map-0.1.2.dist-info → pretix_map-0.1.3.dist-info}/METADATA +1 -1
- {pretix_map-0.1.2.dist-info → pretix_map-0.1.3.dist-info}/RECORD +13 -12
- pretix_mapplugin/__init__.py +1 -1
- pretix_mapplugin/management/commands/geocode_existing_orders.py +153 -107
- pretix_mapplugin/migrations/0002_remove_ordergeocodedata_geocoded_timestamp_and_more.py +32 -0
- pretix_mapplugin/models.py +30 -10
- pretix_mapplugin/static/pretix_mapplugin/css/salesmap.css +40 -7
- pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js +321 -257
- pretix_mapplugin/templates/pretix_mapplugin/map_page.html +40 -17
- {pretix_map-0.1.2.dist-info → pretix_map-0.1.3.dist-info}/WHEEL +0 -0
- {pretix_map-0.1.2.dist-info → pretix_map-0.1.3.dist-info}/entry_points.txt +0 -0
- {pretix_map-0.1.2.dist-info → pretix_map-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {pretix_map-0.1.2.dist-info → pretix_map-0.1.3.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pretix-map
|
3
|
-
Version: 0.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,8 +1,8 @@
|
|
1
|
-
pretix_map-0.1.
|
2
|
-
pretix_mapplugin/__init__.py,sha256=
|
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=
|
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=
|
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=
|
22
|
-
pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js,sha256=
|
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=
|
42
|
-
pretix_map-0.1.
|
43
|
-
pretix_map-0.1.
|
44
|
-
pretix_map-0.1.
|
45
|
-
pretix_map-0.1.
|
46
|
-
pretix_map-0.1.
|
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,,
|
pretix_mapplugin/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.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
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
|
35
|
+
# --- Import your Geocode model and geocoding functions ---
|
35
36
|
from pretix_mapplugin.models import OrderGeocodeData
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
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
|
-
|
65
|
-
|
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
|
-
|
115
|
-
|
116
|
-
|
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
|
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
|
129
|
+
# current_organizer_pk = organizer.pk # No longer needed for task
|
122
130
|
|
123
131
|
with scope(organizer=organizer):
|
124
|
-
# --- Get orders
|
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', 'invoice_address__country', 'event'
|
135
|
+
)
|
126
136
|
|
127
|
-
# --- Filter by event
|
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
|
142
|
+
self.stdout.write(f" Filtering for event: {event.name} ({event_slug})")
|
133
143
|
except Event.DoesNotExist:
|
134
|
-
self.stderr.write(
|
135
|
-
f" Event '{event_slug}' not found
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
if
|
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)
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
-
|
152
|
-
|
153
|
-
|
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
|
-
|
157
|
-
|
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
|
-
|
167
|
-
|
168
|
-
|
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
|
-
|
172
|
-
|
173
|
-
|
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
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
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
|
-
|
184
|
-
|
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
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
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
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
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
|
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
|
-
|
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"
|
222
|
-
|
223
|
-
|
224
|
-
|
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
|
+
]
|
pretix_mapplugin/models.py
CHANGED
@@ -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,
|
12
|
-
related_name='geocode_data',
|
13
|
-
primary_key=True
|
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
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
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 - 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:
|
22
|
+
height: 100%;
|
3
23
|
width: 100%;
|
4
|
-
|
5
|
-
|
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
|
-
#
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
}
|