pretix-map 0.0.4__py3-none-any.whl → 0.0.6__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.0.4.dist-info → pretix_map-0.0.6.dist-info}/METADATA +1 -1
- {pretix_map-0.0.4.dist-info → pretix_map-0.0.6.dist-info}/RECORD +12 -12
- pretix_mapplugin/__init__.py +1 -1
- pretix_mapplugin/geocoding.py +44 -55
- pretix_mapplugin/management/commands/geocode_existing_orders.py +79 -54
- pretix_mapplugin/migrations/0001_initial.py +3 -7
- pretix_mapplugin/signals.py +46 -32
- pretix_mapplugin/tasks.py +64 -47
- {pretix_map-0.0.4.dist-info → pretix_map-0.0.6.dist-info}/WHEEL +0 -0
- {pretix_map-0.0.4.dist-info → pretix_map-0.0.6.dist-info}/entry_points.txt +0 -0
- {pretix_map-0.0.4.dist-info → pretix_map-0.0.6.dist-info}/licenses/LICENSE +0 -0
- {pretix_map-0.0.4.dist-info → pretix_map-0.0.6.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.0.
|
3
|
+
Version: 0.0.6
|
4
4
|
Summary: An overview map of the catchment area of previous orders. Measured by postcode
|
5
5
|
Author-email: MarkenJaden <jjsch1410@gmail.com>
|
6
6
|
Maintainer-email: MarkenJaden <jjsch1410@gmail.com>
|
@@ -1,10 +1,10 @@
|
|
1
|
-
pretix_map-0.0.
|
2
|
-
pretix_mapplugin/__init__.py,sha256=
|
1
|
+
pretix_map-0.0.6.dist-info/licenses/LICENSE,sha256=RhQ89ePNDClBzEROahhwjDrBSEb5Zpx6XewZfGlY4Ss,569
|
2
|
+
pretix_mapplugin/__init__.py,sha256=vnEQ3_faaqtBKoxP4DuJgVUXAxvUIINTpkt3E8qLWks,23
|
3
3
|
pretix_mapplugin/apps.py,sha256=AnThwyRw2AAz5f-kmXZ8hm85OmKnlDkRosVoQOBgPzE,830
|
4
|
-
pretix_mapplugin/geocoding.py,sha256=
|
4
|
+
pretix_mapplugin/geocoding.py,sha256=lBmwMvmE_cPyOHxWE8H3Se2P-2Eq0UjDTCv9gUs97Fo,4018
|
5
5
|
pretix_mapplugin/models.py,sha256=v0v9K0sb5OQHs5Gc6-jea_aEGECUQp1tZoYMwwb3YIM,994
|
6
|
-
pretix_mapplugin/signals.py,sha256=
|
7
|
-
pretix_mapplugin/tasks.py,sha256=
|
6
|
+
pretix_mapplugin/signals.py,sha256=maBMMSq5M7diy_EaNgKr8KRfGFj0U437u-MEl2NVYBw,3661
|
7
|
+
pretix_mapplugin/tasks.py,sha256=dxxEe7WGEGqiwLQlmM7btZTNVTKGQq5L6gqM_RdIecY,4558
|
8
8
|
pretix_mapplugin/urls.py,sha256=o5407vULF4S-bUihU7AeRxUcMyazg2lPjbvqRflsGxE,838
|
9
9
|
pretix_mapplugin/views.py,sha256=7WgmNZeqwmOesT6PrkAIRC8fNfAcWGm-j9-2YqF5egI,7146
|
10
10
|
pretix_mapplugin/locale/de/LC_MESSAGES/django.mo,sha256=6VVRAqa0ixL-lDA1QwoVvG0wd5ZBwYjaR4P8T73hxhU,269
|
@@ -14,8 +14,8 @@ 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=
|
18
|
-
pretix_mapplugin/migrations/0001_initial.py,sha256=
|
17
|
+
pretix_mapplugin/management/commands/geocode_existing_orders.py,sha256=QZGWXZfk-r3qffS3ernnOEsCK3feurc7hBpvCxhwa4U,9416
|
18
|
+
pretix_mapplugin/migrations/0001_initial.py,sha256=KAl1Egxptv1bpregGbsh8wUbr4Yh5A_zazVSAQdmoHM,1020
|
19
19
|
pretix_mapplugin/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
20
20
|
pretix_mapplugin/static/pretix_mapplugin/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
21
21
|
pretix_mapplugin/static/pretix_mapplugin/css/salesmap.css,sha256=z-OXFjpGWOoxv_tlYSDSUlcFLU9p03hhXI-8yxExl3k,598
|
@@ -39,8 +39,8 @@ pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-ic
|
|
39
39
|
pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-shadow.png,sha256=Jk9cZAM58ELdcpBiz8BMF_jqDymIK1OOOEjtjxDttNo,618
|
40
40
|
pretix_mapplugin/templates/pretix_mapplugin/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
41
41
|
pretix_mapplugin/templates/pretix_mapplugin/map_page.html,sha256=jUfPrCkwcbcTXgZ2d9a5wpUD1U7Y8g5rnB20hklKQ-k,2252
|
42
|
-
pretix_map-0.0.
|
43
|
-
pretix_map-0.0.
|
44
|
-
pretix_map-0.0.
|
45
|
-
pretix_map-0.0.
|
46
|
-
pretix_map-0.0.
|
42
|
+
pretix_map-0.0.6.dist-info/METADATA,sha256=LgMrztKJHpMfIbhAF363Ume1LxzKAXc9qzxLYgdOS0Y,9514
|
43
|
+
pretix_map-0.0.6.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
44
|
+
pretix_map-0.0.6.dist-info/entry_points.txt,sha256=C3NAjeZHoCekafkLMCJynPcABRTK8AUprtQv7sUNDZs,137
|
45
|
+
pretix_map-0.0.6.dist-info/top_level.txt,sha256=CAtEnkgA73zE9Gadm5mjt1SpXHBPOS-QWP0dQVoNToE,17
|
46
|
+
pretix_map-0.0.6.dist-info/RECORD,,
|
pretix_mapplugin/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.0.
|
1
|
+
__version__ = "0.0.6"
|
pretix_mapplugin/geocoding.py
CHANGED
@@ -1,62 +1,54 @@
|
|
1
1
|
import logging
|
2
|
-
|
3
|
-
# --- Import Django settings ---
|
4
|
-
from django.conf import settings
|
5
|
-
from geopy.exc import GeocoderServiceError, GeocoderTimedOut
|
6
2
|
from geopy.geocoders import Nominatim
|
3
|
+
from geopy.exc import GeocoderTimedOut, GeocoderServiceError
|
7
4
|
from time import sleep
|
8
5
|
|
9
|
-
#
|
6
|
+
# DO NOT import settings here, as it won't work reliably in Celery
|
7
|
+
|
10
8
|
logger = logging.getLogger(__name__)
|
11
9
|
|
12
|
-
# --- Configuration Default ---
|
13
10
|
# Define a default/fallback User-Agent. Users *should* override this in pretix.cfg.
|
14
11
|
DEFAULT_NOMINATIM_USER_AGENT = "pretix-map-plugin/unknown (Please configure nominatim_user_agent in pretix.cfg)"
|
15
12
|
|
16
13
|
|
17
|
-
# --- Geocoding Function ---
|
18
|
-
|
19
|
-
def geocode_address(address_string: str) -> tuple[float, float] | None:
|
14
|
+
# --- Geocoding Function (Accepts user_agent) ---
|
15
|
+
def geocode_address(address_string: str, nominatim_user_agent: str | None = None) -> tuple[float, float] | None:
|
20
16
|
"""
|
21
|
-
Tries to geocode a given address string using Nominatim,
|
22
|
-
User-Agent
|
17
|
+
Tries to geocode a given address string using Nominatim, using the
|
18
|
+
provided User-Agent string.
|
23
19
|
|
24
20
|
Args:
|
25
21
|
address_string: A single string representing the address.
|
22
|
+
nominatim_user_agent: The User-Agent string to use for Nominatim.
|
26
23
|
|
27
24
|
Returns:
|
28
25
|
A tuple (latitude, longitude) if successful, otherwise None.
|
29
26
|
"""
|
30
|
-
#
|
31
|
-
|
32
|
-
|
33
|
-
user_agent = settings.plugins.pretix_mapplugin.get(
|
34
|
-
'nominatim_user_agent', # The setting name defined in pretix.cfg
|
35
|
-
DEFAULT_NOMINATIM_USER_AGENT
|
36
|
-
)
|
37
|
-
|
38
|
-
# Log a warning if the default User-Agent is being used, as it's required
|
39
|
-
# by Nominatim policy to be specific and include contact info.
|
27
|
+
# Use the provided User-Agent or the default
|
28
|
+
user_agent = nominatim_user_agent or DEFAULT_NOMINATIM_USER_AGENT
|
29
|
+
|
40
30
|
if user_agent == DEFAULT_NOMINATIM_USER_AGENT:
|
31
|
+
# Log warning if default is used - admins should configure this
|
41
32
|
logger.warning(
|
42
33
|
"Using default Nominatim User-Agent. Please set a specific "
|
43
34
|
"'nominatim_user_agent' under [pretix_mapplugin] in your "
|
44
35
|
"pretix.cfg according to Nominatim's usage policy."
|
45
36
|
)
|
46
|
-
# --- End Settings Retrieval ---
|
47
37
|
|
48
|
-
# Initialize the geolocator with the
|
38
|
+
# Initialize the geolocator with the determined user_agent
|
49
39
|
geolocator = Nominatim(user_agent=user_agent)
|
50
40
|
|
51
41
|
try:
|
52
42
|
# Add a 1-second delay to respect Nominatim's usage policy (1 req/sec)
|
53
43
|
sleep(1)
|
54
44
|
|
55
|
-
|
45
|
+
# Perform geocoding
|
46
|
+
location = geolocator.geocode(address_string, timeout=10) # 10-second timeout
|
56
47
|
|
57
48
|
if location:
|
58
49
|
logger.debug(
|
59
|
-
f"Geocoded '{address_string}' to ({location.latitude}, {location.longitude}) using User-Agent: {user_agent}"
|
50
|
+
f"Geocoded '{address_string}' to ({location.latitude}, {location.longitude}) using User-Agent: {user_agent}"
|
51
|
+
)
|
60
52
|
return (location.latitude, location.longitude)
|
61
53
|
else:
|
62
54
|
logger.warning(f"Could not geocode address: {address_string} (Address not found by Nominatim)")
|
@@ -66,48 +58,45 @@ def geocode_address(address_string: str) -> tuple[float, float] | None:
|
|
66
58
|
logger.error(f"Geocoding timed out for address: {address_string}")
|
67
59
|
return None
|
68
60
|
except GeocoderServiceError as e:
|
61
|
+
# Log specific service errors (e.g., API limits, server issues)
|
69
62
|
logger.error(f"Geocoding service error for address '{address_string}': {e}")
|
70
63
|
return None
|
71
64
|
except Exception as e:
|
65
|
+
# Catch any other unexpected exceptions during geocoding
|
72
66
|
logger.exception(f"An unexpected error occurred during geocoding for address '{address_string}': {e}")
|
73
67
|
return None
|
74
68
|
|
75
69
|
|
76
|
-
# --- Helper to Format Address from Pretix Order
|
77
|
-
|
70
|
+
# --- Helper to Format Address from Pretix Order ---
|
78
71
|
def get_formatted_address_from_order(order) -> str | None:
|
79
72
|
"""
|
80
73
|
Creates a formatted address string from a Pretix order's invoice address.
|
81
|
-
"""
|
82
|
-
if not order.invoice_address:
|
83
|
-
return None
|
84
|
-
parts = []
|
85
|
-
if order.invoice_address.street: parts.append(order.invoice_address.street)
|
86
|
-
if order.invoice_address.city: parts.append(order.invoice_address.city)
|
87
|
-
if order.invoice_address.zipcode: parts.append(order.invoice_address.zipcode)
|
88
|
-
if order.invoice_address.state: parts.append(order.invoice_address.state)
|
89
|
-
if order.invoice_address.country: parts.append(str(order.invoice_address.country.name))
|
90
|
-
if not parts: return None
|
91
|
-
full_address = ", ".join(filter(None, parts))
|
92
|
-
return full_address
|
93
74
|
|
75
|
+
Args:
|
76
|
+
order: A Pretix `Order` object.
|
94
77
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
if not address_str:
|
101
|
-
logger.info(f"Order {order.code} has no invoice address to geocode.")
|
78
|
+
Returns:
|
79
|
+
A formatted address string suitable for geocoding, or None if no address.
|
80
|
+
"""
|
81
|
+
# Ensure order and invoice_address exist
|
82
|
+
if not order or not order.invoice_address:
|
102
83
|
return None
|
103
84
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
85
|
+
parts = []
|
86
|
+
addr = order.invoice_address # Shortcut
|
87
|
+
|
88
|
+
# Add components in a likely useful order for geocoding
|
89
|
+
if addr.street: parts.append(addr.street)
|
90
|
+
if addr.city: parts.append(addr.city)
|
91
|
+
if addr.zipcode: parts.append(addr.zipcode)
|
92
|
+
if addr.state: parts.append(addr.state)
|
93
|
+
# Use the full country name if possible, geocoders often prefer it
|
94
|
+
if addr.country: parts.append(str(addr.country.name))
|
95
|
+
|
96
|
+
# Only return an address if we have useful parts
|
97
|
+
if not parts:
|
113
98
|
return None
|
99
|
+
|
100
|
+
# Join parts with commas. Geocoders are usually good at parsing this.
|
101
|
+
full_address = ", ".join(filter(None, parts)) # filter(None,...) removes empty strings
|
102
|
+
return full_address
|
@@ -1,16 +1,36 @@
|
|
1
1
|
import logging
|
2
|
-
from django.core.exceptions import FieldDoesNotExist
|
3
2
|
from django.core.management.base import BaseCommand, CommandError
|
3
|
+
from django.core.exceptions import FieldDoesNotExist
|
4
4
|
|
5
|
-
# Import
|
5
|
+
# --- Import Pretix Global Settings accessor ---
|
6
6
|
from django_scopes import scope
|
7
7
|
|
8
|
-
#
|
9
|
-
|
8
|
+
# Check if Pretix version uses AbstractSettingsHolder or GlobalSettingsObject
|
9
|
+
# Adjust import based on Pretix version if needed. Assume AbstractSettingsHolder for newer Pretix.
|
10
|
+
try:
|
11
|
+
from pretix.base.settings import GlobalSettingsObject as SettingsProxy
|
12
|
+
except ImportError:
|
13
|
+
try:
|
14
|
+
# Older pretix might use this pattern
|
15
|
+
from pretix.base.services.config import load_config
|
16
|
+
|
10
17
|
|
11
|
-
|
18
|
+
class SettingsProxy:
|
19
|
+
def __init__(self):
|
20
|
+
self.settings = load_config()
|
21
|
+
except ImportError:
|
22
|
+
# Fallback or raise error if neither is found
|
23
|
+
logger.error("Could not determine Pretix settings accessor for management command.")
|
24
|
+
raise ImportError("Cannot find Pretix settings accessor.")
|
25
|
+
|
26
|
+
# --- Import necessary Pretix models ---
|
27
|
+
from pretix.base.models import Order, Event, Organizer
|
28
|
+
|
29
|
+
# --- Import your Geocode model and the task ---
|
12
30
|
from pretix_mapplugin.models import OrderGeocodeData
|
13
31
|
from pretix_mapplugin.tasks import geocode_order_task
|
32
|
+
# --- Import Default User-Agent ---
|
33
|
+
from pretix_mapplugin.geocoding import DEFAULT_NOMINATIM_USER_AGENT
|
14
34
|
|
15
35
|
logger = logging.getLogger(__name__)
|
16
36
|
|
@@ -20,23 +40,16 @@ class Command(BaseCommand):
|
|
20
40
|
|
21
41
|
def add_arguments(self, parser):
|
22
42
|
parser.add_argument(
|
23
|
-
'--organizer',
|
24
|
-
type=str,
|
25
|
-
help='Slug of a specific organizer to process orders for.',
|
43
|
+
'--organizer', type=str, help='Slug of a specific organizer to process orders for.',
|
26
44
|
)
|
27
45
|
parser.add_argument(
|
28
|
-
'--event',
|
29
|
-
type=str,
|
30
|
-
help='Slug of a specific event to process orders for. Requires --organizer.',
|
46
|
+
'--event', type=str, help='Slug of a specific event to process orders for. Requires --organizer.',
|
31
47
|
)
|
32
48
|
parser.add_argument(
|
33
|
-
'--dry-run',
|
34
|
-
action='store_true',
|
35
|
-
help='Simulate the process without actually queuing tasks.',
|
49
|
+
'--dry-run', action='store_true', help='Simulate the process without actually queuing tasks.',
|
36
50
|
)
|
37
51
|
parser.add_argument(
|
38
|
-
'--force-recode',
|
39
|
-
action='store_true',
|
52
|
+
'--force-recode', action='store_true',
|
40
53
|
help='Queue geocoding even for orders that already have geocode data.',
|
41
54
|
)
|
42
55
|
|
@@ -49,86 +62,97 @@ class Command(BaseCommand):
|
|
49
62
|
if event_slug and not organizer_slug:
|
50
63
|
raise CommandError("You must specify --organizer when using --event.")
|
51
64
|
|
65
|
+
# --- Read User-Agent using Pretix Settings accessor ---
|
66
|
+
user_agent = DEFAULT_NOMINATIM_USER_AGENT
|
67
|
+
try:
|
68
|
+
# Get settings holder instance
|
69
|
+
gs = SettingsProxy()
|
70
|
+
# Construct the setting key specific to plugins
|
71
|
+
# Format might be 'plugin:plugin_name:setting_name' or just 'plugin_name_setting_name'
|
72
|
+
# Check Pretix docs or experiment if needed. Assuming the former.
|
73
|
+
setting_key = 'plugin:pretix_mapplugin:nominatim_user_agent'
|
74
|
+
user_agent = gs.settings.get(setting_key, DEFAULT_NOMINATIM_USER_AGENT)
|
75
|
+
|
76
|
+
if user_agent == DEFAULT_NOMINATIM_USER_AGENT:
|
77
|
+
self.stdout.write(self.style.WARNING(
|
78
|
+
"Using default Nominatim User-Agent. Please set a specific "
|
79
|
+
f"'{setting_key}' in your pretix.cfg."
|
80
|
+
))
|
81
|
+
except Exception as e:
|
82
|
+
# Catch broad exception during settings access
|
83
|
+
self.stderr.write(self.style.ERROR(f"Failed to read plugin settings: {e}. Using default User-Agent."))
|
84
|
+
# Continue with default user_agent
|
85
|
+
# --- End Read User-Agent ---
|
86
|
+
|
52
87
|
# --- Determine which organizers to process ---
|
53
88
|
organizers_to_process = []
|
54
89
|
if organizer_slug:
|
55
90
|
try:
|
56
|
-
# Fetch specific organizer (outside scope)
|
57
91
|
organizer = Organizer.objects.get(slug=organizer_slug)
|
58
92
|
organizers_to_process.append(organizer)
|
59
93
|
self.stdout.write(f"Processing specified organizer: {organizer.name} ({organizer_slug})")
|
60
94
|
except Organizer.DoesNotExist:
|
61
95
|
raise CommandError(f"Organizer with slug '{organizer_slug}' not found.")
|
62
96
|
else:
|
63
|
-
# Fetch all organizers (outside scope)
|
64
97
|
organizers_to_process = list(Organizer.objects.all())
|
65
98
|
self.stdout.write(f"Processing all {len(organizers_to_process)} organizers...")
|
66
99
|
|
67
100
|
# --- Initialize counters ---
|
68
101
|
total_queued = 0
|
69
102
|
total_skipped = 0
|
70
|
-
total_processed_orders = 0
|
103
|
+
total_processed_orders = 0
|
71
104
|
|
72
105
|
# --- Iterate through organizers and activate scope ---
|
73
106
|
for organizer in organizers_to_process:
|
74
107
|
self.stdout.write(f"\n--- Processing Organizer: {organizer.name} ({organizer.slug}) ---")
|
75
108
|
|
76
|
-
# --- Activate scope for this organizer ---
|
77
109
|
with scope(organizer=organizer):
|
78
|
-
# ---
|
79
|
-
|
80
|
-
# Start with paid orders FOR THIS ORGANIZER
|
110
|
+
# --- Get orders ---
|
81
111
|
orders_qs = Order.objects.filter(status=Order.STATUS_PAID)
|
82
112
|
|
83
|
-
# Filter by
|
84
|
-
if event_slug and organizer.slug == organizer_slug:
|
113
|
+
# --- Filter by event if specified ---
|
114
|
+
if event_slug and organizer.slug == organizer_slug:
|
85
115
|
try:
|
86
|
-
|
87
|
-
event = Event.objects.get(slug=event_slug) # No need for organizer filter here
|
116
|
+
event = Event.objects.get(slug=event_slug)
|
88
117
|
orders_qs = orders_qs.filter(event=event)
|
89
118
|
self.stdout.write(f" Filtering orders for event: {event.name} ({event_slug})")
|
90
119
|
except Event.DoesNotExist:
|
91
|
-
# Don't raise CommandError, just report and skip event for this organizer
|
92
120
|
self.stderr.write(self.style.WARNING(
|
93
121
|
f" Event '{event_slug}' not found for this organizer. Skipping event filter."))
|
94
|
-
|
95
|
-
if organizer_slug and event_slug:
|
96
|
-
continue
|
122
|
+
if organizer_slug and event_slug: continue
|
97
123
|
|
98
|
-
# Filter orders needing geocoding
|
124
|
+
# --- Filter orders needing geocoding ---
|
125
|
+
relation_name = 'geocode_data' # Ensure this matches your model
|
99
126
|
if not force_recode:
|
100
127
|
try:
|
101
|
-
# Check
|
102
|
-
relation_name = 'geocode_data' # Change if necessary
|
103
|
-
Order._meta.get_field(relation_name)
|
128
|
+
Order._meta.get_field(relation_name) # Check existence
|
104
129
|
orders_to_process_qs = orders_qs.filter(**{f'{relation_name}__isnull': True})
|
105
130
|
self.stdout.write(" Selecting paid orders missing geocode data...")
|
106
131
|
except FieldDoesNotExist:
|
107
|
-
self.stderr.write(self.style.ERROR(
|
108
|
-
f" Could not find reverse relation '{relation_name}' on Order model. Check OrderGeocodeData model. Skipping organizer."))
|
109
|
-
continue # Skip this organizer if relation is wrong
|
110
|
-
except Exception as e:
|
111
132
|
self.stderr.write(
|
112
|
-
self.style.ERROR(f"
|
133
|
+
self.style.ERROR(f" Relation '{relation_name}' not found. Skipping organizer."))
|
134
|
+
continue
|
135
|
+
except Exception as e:
|
136
|
+
self.stderr.write(self.style.ERROR(f" Error checking relation: {e}. Skipping organizer."))
|
113
137
|
continue
|
114
138
|
else:
|
115
139
|
orders_to_process_qs = orders_qs
|
116
|
-
self.stdout.write(self.style.WARNING(
|
117
|
-
" Processing ALL selected paid orders for this organizer (--force-recode)..."))
|
140
|
+
self.stdout.write(self.style.WARNING(" Processing ALL selected paid orders (--force-recode)..."))
|
118
141
|
|
119
|
-
#
|
142
|
+
# --- Process orders for this scope ---
|
120
143
|
current_org_orders_count = orders_to_process_qs.count()
|
121
|
-
|
144
|
+
all_checked_for_org = orders_qs.count()
|
145
|
+
total_processed_orders += all_checked_for_org
|
122
146
|
|
123
147
|
if current_org_orders_count == 0:
|
124
|
-
self.stdout.write(" No orders need geocoding
|
125
|
-
continue
|
148
|
+
self.stdout.write(f" No orders need geocoding ({all_checked_for_org} checked).")
|
149
|
+
continue
|
126
150
|
|
127
|
-
self.stdout.write(
|
151
|
+
self.stdout.write(
|
152
|
+
f" Found {current_org_orders_count} order(s) to potentially geocode ({all_checked_for_org} checked).")
|
128
153
|
org_queued = 0
|
129
154
|
org_skipped = 0
|
130
155
|
|
131
|
-
# Iterate and queue (within scope)
|
132
156
|
for order in orders_to_process_qs.iterator():
|
133
157
|
if dry_run:
|
134
158
|
self.stdout.write(
|
@@ -136,9 +160,11 @@ class Command(BaseCommand):
|
|
136
160
|
org_queued += 1
|
137
161
|
else:
|
138
162
|
try:
|
139
|
-
|
140
|
-
|
141
|
-
|
163
|
+
# --- Pass user_agent to task ---
|
164
|
+
geocode_order_task.apply_async(
|
165
|
+
args=[order.pk],
|
166
|
+
kwargs={'nominatim_user_agent': user_agent} # Pass as kwarg
|
167
|
+
)
|
142
168
|
org_queued += 1
|
143
169
|
except Exception as e:
|
144
170
|
self.stderr.write(self.style.ERROR(f" ERROR queuing Order {order.code}: {e}"))
|
@@ -148,14 +174,13 @@ class Command(BaseCommand):
|
|
148
174
|
self.stdout.write(f" Queued: {org_queued}, Skipped: {org_skipped} for this organizer.")
|
149
175
|
total_queued += org_queued
|
150
176
|
total_skipped += org_skipped
|
151
|
-
|
152
|
-
# Scope for 'organizer' is automatically deactivated here by 'with' statement
|
177
|
+
# End scope
|
153
178
|
|
154
179
|
# --- Final Report ---
|
155
180
|
self.stdout.write("=" * 40)
|
156
181
|
self.stdout.write("Overall Summary:")
|
157
182
|
self.stdout.write(f" Organizers processed: {len(organizers_to_process)}")
|
158
|
-
self.stdout.write(f" Total orders checked (paid): {total_processed_orders}")
|
183
|
+
self.stdout.write(f" Total orders checked (paid): {total_processed_orders}")
|
159
184
|
if dry_run:
|
160
185
|
self.stdout.write(
|
161
186
|
self.style.SUCCESS(f"[DRY RUN] Complete. Would have queued tasks for {total_queued} order(s)."))
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Generated by Django 4.2.20 on 2025-04-15
|
1
|
+
# Generated by Django 4.2.20 on 2025-04-15 23:21
|
2
2
|
|
3
3
|
from django.db import migrations, models
|
4
4
|
import django.db.models.deletion
|
@@ -6,18 +6,14 @@ import pretix.base.models.base
|
|
6
6
|
|
7
7
|
|
8
8
|
class Migration(migrations.Migration):
|
9
|
-
|
10
9
|
initial = True
|
11
10
|
|
12
|
-
dependencies = [
|
13
|
-
('pretixbase', '0280_alter_customer_locale_alter_user_locale'),
|
14
|
-
]
|
15
|
-
|
16
11
|
operations = [
|
17
12
|
migrations.CreateModel(
|
18
13
|
name='OrderGeocodeData',
|
19
14
|
fields=[
|
20
|
-
('order', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True,
|
15
|
+
('order', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True,
|
16
|
+
related_name='geocode_data', serialize=False, to='pretixbase.order')),
|
21
17
|
('latitude', models.FloatField()),
|
22
18
|
('longitude', models.FloatField()),
|
23
19
|
('geocoded_timestamp', models.DateTimeField(auto_now_add=True)),
|
pretix_mapplugin/signals.py
CHANGED
@@ -1,51 +1,73 @@
|
|
1
1
|
import logging
|
2
2
|
from django.dispatch import receiver
|
3
|
-
from django.
|
4
|
-
from django.
|
5
|
-
from django.
|
3
|
+
from django.urls import reverse, NoReverseMatch
|
4
|
+
from django.utils.translation import gettext_lazy as _
|
5
|
+
from django.http import HttpRequest
|
6
|
+
# Import Django settings to read config in web process
|
7
|
+
from django.conf import settings
|
6
8
|
|
7
9
|
# --- Pretix Signals ---
|
8
10
|
from pretix.base.signals import order_paid
|
9
|
-
from pretix.control.signals import nav_event
|
11
|
+
from pretix.control.signals import nav_event
|
10
12
|
|
11
13
|
# --- Tasks ---
|
12
14
|
from .tasks import geocode_order_task
|
15
|
+
# --- Geocoding Default ---
|
16
|
+
from .geocoding import DEFAULT_NOMINATIM_USER_AGENT # Import default
|
13
17
|
|
14
18
|
logger = logging.getLogger(__name__)
|
15
19
|
|
16
20
|
# --- Constants ---
|
17
21
|
MAP_VIEW_URL_NAME = 'plugins:pretix_mapplugin:event.settings.salesmap.show'
|
18
|
-
# Define the permission required to see the map link
|
19
22
|
REQUIRED_MAP_PERMISSION = 'can_view_orders'
|
23
|
+
PLUGIN_NAME = 'pretix_mapplugin' # Define plugin name for settings access
|
20
24
|
|
21
25
|
|
22
|
-
# --- Signal Receiver for Geocoding (
|
26
|
+
# --- Signal Receiver for Geocoding (Reads setting, passes to task) ---
|
23
27
|
@receiver(order_paid, dispatch_uid="sales_mapper_order_paid_geocode")
|
24
28
|
def trigger_geocoding_on_payment(sender, order, **kwargs):
|
25
|
-
|
29
|
+
"""
|
30
|
+
Listens for the order_paid signal, reads geocoding config,
|
31
|
+
and queues the geocoding task with the config.
|
32
|
+
"""
|
33
|
+
user_agent = DEFAULT_NOMINATIM_USER_AGENT # Start with default
|
26
34
|
try:
|
27
|
-
|
35
|
+
# --- Read User-Agent from settings (works in web process) ---
|
36
|
+
# Check structure defensively before accessing
|
37
|
+
if hasattr(settings, 'plugins') and hasattr(settings.plugins, PLUGIN_NAME):
|
38
|
+
plugin_settings = getattr(settings.plugins, PLUGIN_NAME)
|
39
|
+
user_agent = plugin_settings.get(
|
40
|
+
'nominatim_user_agent', # Setting name in pretix.cfg
|
41
|
+
DEFAULT_NOMINATIM_USER_AGENT
|
42
|
+
)
|
43
|
+
else:
|
44
|
+
logger.warning(
|
45
|
+
f"Could not access settings.plugins.{PLUGIN_NAME}, "
|
46
|
+
"using default Nominatim User-Agent for task."
|
47
|
+
)
|
48
|
+
|
49
|
+
# --- Queue task with user_agent as keyword argument ---
|
50
|
+
geocode_order_task.apply_async(
|
51
|
+
args=[order.pk],
|
52
|
+
kwargs={'nominatim_user_agent': user_agent} # Pass as kwarg
|
53
|
+
)
|
28
54
|
logger.info(f"Geocoding task queued for paid order {order.code} (PK: {order.pk}).")
|
29
|
-
|
30
|
-
|
55
|
+
|
56
|
+
except ImportError: # Error finding geocode_order_task itself if tasks.py fails
|
57
|
+
logger.exception("Could not import geocode_order_task. Check tasks.py.")
|
31
58
|
except Exception as e:
|
32
59
|
logger.exception(f"Failed to queue geocoding task for order {order.code}: {e}")
|
33
60
|
|
34
61
|
|
35
|
-
# --- Signal Receiver for Adding Navigation Item ---
|
62
|
+
# --- Signal Receiver for Adding Navigation Item (No changes needed) ---
|
36
63
|
@receiver(nav_event, dispatch_uid="sales_mapper_nav_event_add_map")
|
37
64
|
def add_map_nav_item(sender, request: HttpRequest, **kwargs):
|
38
65
|
"""
|
39
66
|
Adds a navigation item for the Sales Map to the event control panel sidebar.
|
40
67
|
"""
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
)
|
45
|
-
if not has_permission:
|
46
|
-
return [] # Return empty list if user lacks permission
|
47
|
-
|
48
|
-
# Try to generate the URL for the map view
|
68
|
+
has_permission = request.user.has_event_permission(request.organizer, request.event, REQUIRED_MAP_PERMISSION,
|
69
|
+
request=request)
|
70
|
+
if not has_permission: return []
|
49
71
|
try:
|
50
72
|
map_url = reverse(MAP_VIEW_URL_NAME, kwargs={
|
51
73
|
'organizer': request.organizer.slug,
|
@@ -53,21 +75,13 @@ def add_map_nav_item(sender, request: HttpRequest, **kwargs):
|
|
53
75
|
})
|
54
76
|
except NoReverseMatch:
|
55
77
|
logger.error(f"Could not reverse URL for map view '{MAP_VIEW_URL_NAME}'. Check urls.py.")
|
56
|
-
return []
|
57
|
-
|
58
|
-
# Check if the current page *is* the map page to set the 'active' state
|
78
|
+
return []
|
59
79
|
is_active = False
|
60
80
|
if hasattr(request, 'resolver_match') and request.resolver_match:
|
61
81
|
is_active = request.resolver_match.view_name == MAP_VIEW_URL_NAME
|
62
|
-
|
63
|
-
|
64
|
-
nav_item = {
|
65
|
-
'label': _('Sales Map'), # Translatable label
|
82
|
+
return [{
|
83
|
+
'label': _('Sales Map'),
|
66
84
|
'url': map_url,
|
67
85
|
'active': is_active,
|
68
|
-
'icon': 'map-o',
|
69
|
-
|
70
|
-
}
|
71
|
-
|
72
|
-
# Return the item in a list
|
73
|
-
return [nav_item]
|
86
|
+
'icon': 'map-o',
|
87
|
+
}]
|
pretix_mapplugin/tasks.py
CHANGED
@@ -1,74 +1,91 @@
|
|
1
1
|
import logging
|
2
|
+
from django.db import transaction
|
3
|
+
from django.core.exceptions import ObjectDoesNotExist
|
4
|
+
|
5
|
+
# --- Use Pretix Celery app instance ---
|
6
|
+
from pretix.celery_app import app
|
7
|
+
# --- Import necessary Pretix models ---
|
2
8
|
from pretix.base.models import Order
|
3
|
-
from pretix.celery_app import app # Import the Pretix Celery app instance
|
4
9
|
|
5
|
-
|
6
|
-
|
10
|
+
# --- Import your Geocode model and geocoding functions ---
|
11
|
+
from .models import OrderGeocodeData
|
12
|
+
from .geocoding import (
|
7
13
|
get_formatted_address_from_order,
|
14
|
+
geocode_address,
|
15
|
+
DEFAULT_NOMINATIM_USER_AGENT # Import default for safety/logging
|
8
16
|
)
|
9
|
-
from .models import OrderGeocodeData
|
10
17
|
|
11
18
|
logger = logging.getLogger(__name__)
|
12
19
|
|
13
|
-
|
14
|
-
|
20
|
+
|
21
|
+
# Define the Celery task
|
22
|
+
# bind=True gives access to self (the task instance) for retrying
|
23
|
+
# ignore_result=True as we don't need the return value stored in Celery backend
|
24
|
+
@app.task(bind=True, max_retries=3, default_retry_delay=60, ignore_result=True)
|
25
|
+
def geocode_order_task(self, order_pk: int,
|
26
|
+
nominatim_user_agent: str | None = None): # Added nominatim_user_agent kwarg
|
15
27
|
"""
|
16
|
-
Celery task to geocode
|
28
|
+
Celery task to geocode the address for a given order PK.
|
29
|
+
Accepts the Nominatim User-Agent as an argument.
|
17
30
|
"""
|
18
31
|
try:
|
19
|
-
order
|
32
|
+
# Fetch order with related address and country data efficiently
|
33
|
+
order = Order.objects.select_related(
|
34
|
+
'invoice_address',
|
35
|
+
'invoice_address__country'
|
36
|
+
).get(pk=order_pk)
|
20
37
|
logger.info(f"Starting geocoding task for Order {order.code} (PK: {order_pk})")
|
21
38
|
|
22
|
-
# Check if already geocoded to prevent redundant work
|
23
|
-
if
|
39
|
+
# Check if already geocoded to prevent redundant work
|
40
|
+
# Replace 'geocode_data' if your related_name is different
|
41
|
+
relation_name = 'geocode_data' # Ensure this matches your OrderGeocodeData.order related_name
|
42
|
+
if hasattr(order, relation_name) and getattr(order, relation_name) is not None:
|
24
43
|
logger.info(f"Geocode data already exists for Order {order.code}. Skipping.")
|
25
|
-
return
|
44
|
+
return # Exit successfully
|
26
45
|
|
27
|
-
# 1. Get formatted address
|
46
|
+
# 1. Get formatted address string
|
28
47
|
address_str = get_formatted_address_from_order(order)
|
29
48
|
if not address_str:
|
30
|
-
logger.
|
31
|
-
|
49
|
+
logger.info(f"Order {order.code} has no address suitable for geocoding. Storing null coordinates.")
|
50
|
+
# Store null to prevent reprocessing
|
51
|
+
with transaction.atomic():
|
52
|
+
OrderGeocodeData.objects.update_or_create(
|
53
|
+
order=order,
|
54
|
+
defaults={'latitude': None, 'longitude': None}
|
55
|
+
)
|
56
|
+
return # Exit successfully, nothing to geocode
|
32
57
|
|
33
|
-
# 2. Perform geocoding
|
58
|
+
# 2. Perform geocoding, passing the user agent received by the task
|
34
59
|
logger.debug(f"Attempting to geocode address for Order {order.code}: '{address_str}'")
|
35
|
-
coordinates = geocode_address(address_str)
|
60
|
+
coordinates = geocode_address(address_str, nominatim_user_agent=nominatim_user_agent)
|
36
61
|
|
37
|
-
# 3. Store result if
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
# Use update_or_create to handle potential race conditions gracefully,
|
42
|
-
# although the initial check makes it less likely.
|
62
|
+
# 3. Store result (or null if failed) using atomic transaction
|
63
|
+
with transaction.atomic():
|
64
|
+
if coordinates:
|
65
|
+
latitude, longitude = coordinates
|
43
66
|
obj, created = OrderGeocodeData.objects.update_or_create(
|
44
67
|
order=order,
|
45
|
-
defaults={
|
46
|
-
'latitude': latitude,
|
47
|
-
'longitude': longitude
|
48
|
-
}
|
68
|
+
defaults={'latitude': latitude, 'longitude': longitude}
|
49
69
|
)
|
50
|
-
if created
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
# For example, don't retry if address was not found, but maybe retry on timeout.
|
64
|
-
# The geocode_address function would need to return more info for that.
|
65
|
-
# For now, we just log and don't store anything.
|
70
|
+
log_level = logging.INFO if created else logging.DEBUG # Be less noisy on updates
|
71
|
+
logger.log(log_level,
|
72
|
+
f"Saved{' new' if created else ' updated'} geocode data for Order {order.code}: ({latitude}, {longitude})")
|
73
|
+
else:
|
74
|
+
logger.warning(f"Geocoding failed for Order {order.code}. Storing null coordinates.")
|
75
|
+
# Store nulls to indicate an attempt was made and failed
|
76
|
+
obj, created = OrderGeocodeData.objects.update_or_create(
|
77
|
+
order=order,
|
78
|
+
defaults={'latitude': None, 'longitude': None}
|
79
|
+
)
|
80
|
+
log_level = logging.INFO if created else logging.DEBUG
|
81
|
+
logger.log(log_level,
|
82
|
+
f"Saved{' new' if created else ' updated'} null geocode data for Order {order.code} after failed attempt.")
|
66
83
|
|
67
|
-
except
|
68
|
-
logger.error(f"Order with PK {order_pk} not found
|
84
|
+
except ObjectDoesNotExist: # More specific exception
|
85
|
+
logger.error(f"Order with PK {order_pk} not found in geocode_order_task.")
|
69
86
|
# Don't retry if the order doesn't exist
|
70
87
|
except Exception as e:
|
71
|
-
# Catch any other unexpected errors
|
88
|
+
# Catch any other unexpected errors
|
72
89
|
logger.exception(f"Unexpected error in geocode_order_task for Order PK {order_pk}: {e}")
|
73
|
-
# Retry on
|
74
|
-
self.retry(exc=e)
|
90
|
+
# Retry on potentially temporary errors (database, network issues etc.)
|
91
|
+
raise self.retry(exc=e) # Let Celery handle retry logic
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|