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