pretix-map 0.0.4__tar.gz → 0.0.6__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.
Files changed (58) hide show
  1. {pretix_map-0.0.4/pretix_map.egg-info → pretix_map-0.0.6}/PKG-INFO +1 -1
  2. {pretix_map-0.0.4 → pretix_map-0.0.6/pretix_map.egg-info}/PKG-INFO +1 -1
  3. pretix_map-0.0.6/pretix_mapplugin/__init__.py +1 -0
  4. pretix_map-0.0.6/pretix_mapplugin/geocoding.py +102 -0
  5. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/management/commands/geocode_existing_orders.py +79 -54
  6. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/migrations/0001_initial.py +3 -7
  7. pretix_map-0.0.6/pretix_mapplugin/signals.py +87 -0
  8. pretix_map-0.0.6/pretix_mapplugin/tasks.py +91 -0
  9. pretix_map-0.0.4/pretix_mapplugin/__init__.py +0 -1
  10. pretix_map-0.0.4/pretix_mapplugin/geocoding.py +0 -113
  11. pretix_map-0.0.4/pretix_mapplugin/signals.py +0 -73
  12. pretix_map-0.0.4/pretix_mapplugin/tasks.py +0 -74
  13. {pretix_map-0.0.4 → pretix_map-0.0.6}/LICENSE +0 -0
  14. {pretix_map-0.0.4 → pretix_map-0.0.6}/MANIFEST.in +0 -0
  15. {pretix_map-0.0.4 → pretix_map-0.0.6}/README.rst +0 -0
  16. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_map.egg-info/SOURCES.txt +0 -0
  17. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_map.egg-info/dependency_links.txt +0 -0
  18. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_map.egg-info/entry_points.txt +0 -0
  19. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_map.egg-info/requires.txt +0 -0
  20. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_map.egg-info/top_level.txt +0 -0
  21. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/apps.py +0 -0
  22. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/locale/de/LC_MESSAGES/django.mo +0 -0
  23. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/locale/de/LC_MESSAGES/django.po +0 -0
  24. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/locale/de_Informal/.gitkeep +0 -0
  25. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.mo +0 -0
  26. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.po +0 -0
  27. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/management/__init__.py +0 -0
  28. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/management/commands/__init__.py +0 -0
  29. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/migrations/__init__.py +0 -0
  30. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/models.py +0 -0
  31. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/static/pretix_mapplugin/.gitkeep +0 -0
  32. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/static/pretix_mapplugin/css/salesmap.css +0 -0
  33. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js +0 -0
  34. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.Default.css +0 -0
  35. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.css +0 -0
  36. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/layers-2x.png +0 -0
  37. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/layers.png +0 -0
  38. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-icon-2x.png +0 -0
  39. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-icon.png +0 -0
  40. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/images/marker-shadow.png +0 -0
  41. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-heat.js +0 -0
  42. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.esm.js +0 -0
  43. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.esm.js.map +0 -0
  44. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.js +0 -0
  45. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.js.map +0 -0
  46. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.css +0 -0
  47. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.js +0 -0
  48. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.js.map +0 -0
  49. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js +0 -0
  50. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js.map +0 -0
  51. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/templates/pretix_mapplugin/.gitkeep +0 -0
  52. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/templates/pretix_mapplugin/map_page.html +0 -0
  53. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/urls.py +0 -0
  54. {pretix_map-0.0.4 → pretix_map-0.0.6}/pretix_mapplugin/views.py +0 -0
  55. {pretix_map-0.0.4 → pretix_map-0.0.6}/pyproject.toml +0 -0
  56. {pretix_map-0.0.4 → pretix_map-0.0.6}/setup.cfg +0 -0
  57. {pretix_map-0.0.4 → pretix_map-0.0.6}/setup.py +0 -0
  58. {pretix_map-0.0.4 → pretix_map-0.0.6}/tests/test_main.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pretix-map
3
- Version: 0.0.4
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pretix-map
3
- Version: 0.0.4
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>
@@ -0,0 +1 @@
1
+ __version__ = "0.0.6"
@@ -0,0 +1,102 @@
1
+ import logging
2
+ from geopy.geocoders import Nominatim
3
+ from geopy.exc import GeocoderTimedOut, GeocoderServiceError
4
+ from time import sleep
5
+
6
+ # DO NOT import settings here, as it won't work reliably in Celery
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ # Define a default/fallback User-Agent. Users *should* override this in pretix.cfg.
11
+ DEFAULT_NOMINATIM_USER_AGENT = "pretix-map-plugin/unknown (Please configure nominatim_user_agent in pretix.cfg)"
12
+
13
+
14
+ # --- Geocoding Function (Accepts user_agent) ---
15
+ def geocode_address(address_string: str, nominatim_user_agent: str | None = None) -> tuple[float, float] | None:
16
+ """
17
+ Tries to geocode a given address string using Nominatim, using the
18
+ provided User-Agent string.
19
+
20
+ Args:
21
+ address_string: A single string representing the address.
22
+ nominatim_user_agent: The User-Agent string to use for Nominatim.
23
+
24
+ Returns:
25
+ A tuple (latitude, longitude) if successful, otherwise None.
26
+ """
27
+ # Use the provided User-Agent or the default
28
+ user_agent = nominatim_user_agent or DEFAULT_NOMINATIM_USER_AGENT
29
+
30
+ if user_agent == DEFAULT_NOMINATIM_USER_AGENT:
31
+ # Log warning if default is used - admins should configure this
32
+ logger.warning(
33
+ "Using default Nominatim User-Agent. Please set a specific "
34
+ "'nominatim_user_agent' under [pretix_mapplugin] in your "
35
+ "pretix.cfg according to Nominatim's usage policy."
36
+ )
37
+
38
+ # Initialize the geolocator with the determined user_agent
39
+ geolocator = Nominatim(user_agent=user_agent)
40
+
41
+ try:
42
+ # Add a 1-second delay to respect Nominatim's usage policy (1 req/sec)
43
+ sleep(1)
44
+
45
+ # Perform geocoding
46
+ location = geolocator.geocode(address_string, timeout=10) # 10-second timeout
47
+
48
+ if location:
49
+ logger.debug(
50
+ f"Geocoded '{address_string}' to ({location.latitude}, {location.longitude}) using User-Agent: {user_agent}"
51
+ )
52
+ return (location.latitude, location.longitude)
53
+ else:
54
+ logger.warning(f"Could not geocode address: {address_string} (Address not found by Nominatim)")
55
+ return None
56
+
57
+ except GeocoderTimedOut:
58
+ logger.error(f"Geocoding timed out for address: {address_string}")
59
+ return None
60
+ except GeocoderServiceError as e:
61
+ # Log specific service errors (e.g., API limits, server issues)
62
+ logger.error(f"Geocoding service error for address '{address_string}': {e}")
63
+ return None
64
+ except Exception as e:
65
+ # Catch any other unexpected exceptions during geocoding
66
+ logger.exception(f"An unexpected error occurred during geocoding for address '{address_string}': {e}")
67
+ return None
68
+
69
+
70
+ # --- Helper to Format Address from Pretix Order ---
71
+ def get_formatted_address_from_order(order) -> str | None:
72
+ """
73
+ Creates a formatted address string from a Pretix order's invoice address.
74
+
75
+ Args:
76
+ order: A Pretix `Order` object.
77
+
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:
83
+ return None
84
+
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:
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 scope activation helpers
5
+ # --- Import Pretix Global Settings accessor ---
6
6
  from django_scopes import scope
7
7
 
8
- # Import necessary Pretix models
9
- from pretix.base.models import Event, Order, Organizer
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
- # Import your Geocode model and the task
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 # Track how many orders were checked
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
- # --- Now perform queries WITHIN the scope ---
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 specific Event if requested
84
- if event_slug and organizer.slug == organizer_slug: # Ensure we only filter for the specified org
113
+ # --- Filter by event if specified ---
114
+ if event_slug and organizer.slug == organizer_slug:
85
115
  try:
86
- # Event query is now safe within organizer scope
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
- # If only this event was requested for this organizer, skip to next organizer
95
- if organizer_slug and event_slug:
96
- continue
122
+ if organizer_slug and event_slug: continue
97
123
 
98
- # Filter orders needing geocoding (within scope)
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 relation name - REPLACE 'geocode_data' if yours is different
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" Unexpected error checking relation: {e}. Skipping organizer."))
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
- # Get count within scope
142
+ # --- Process orders for this scope ---
120
143
  current_org_orders_count = orders_to_process_qs.count()
121
- total_processed_orders += orders_qs.count() # Count all checked orders for this org
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 for this organizer/event.")
125
- continue # Skip to next organizer
148
+ self.stdout.write(f" No orders need geocoding ({all_checked_for_org} checked).")
149
+ continue
126
150
 
127
- self.stdout.write(f" Found {current_org_orders_count} order(s) to potentially geocode.")
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
- geocode_order_task.apply_async(args=[order.pk])
140
- # Be slightly less verbose inside the loop
141
- # self.stdout.write(f" Queued Order: {order.code} (PK: {order.pk})")
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}") # Report total checked
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 22:47
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, related_name='geocode_data', serialize=False, to='pretixbase.order')),
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)),
@@ -0,0 +1,87 @@
1
+ import logging
2
+ from django.dispatch import receiver
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
8
+
9
+ # --- Pretix Signals ---
10
+ from pretix.base.signals import order_paid
11
+ from pretix.control.signals import nav_event
12
+
13
+ # --- Tasks ---
14
+ from .tasks import geocode_order_task
15
+ # --- Geocoding Default ---
16
+ from .geocoding import DEFAULT_NOMINATIM_USER_AGENT # Import default
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # --- Constants ---
21
+ MAP_VIEW_URL_NAME = 'plugins:pretix_mapplugin:event.settings.salesmap.show'
22
+ REQUIRED_MAP_PERMISSION = 'can_view_orders'
23
+ PLUGIN_NAME = 'pretix_mapplugin' # Define plugin name for settings access
24
+
25
+
26
+ # --- Signal Receiver for Geocoding (Reads setting, passes to task) ---
27
+ @receiver(order_paid, dispatch_uid="sales_mapper_order_paid_geocode")
28
+ def trigger_geocoding_on_payment(sender, order, **kwargs):
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
34
+ try:
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
+ )
54
+ logger.info(f"Geocoding task queued for paid order {order.code} (PK: {order.pk}).")
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.")
58
+ except Exception as e:
59
+ logger.exception(f"Failed to queue geocoding task for order {order.code}: {e}")
60
+
61
+
62
+ # --- Signal Receiver for Adding Navigation Item (No changes needed) ---
63
+ @receiver(nav_event, dispatch_uid="sales_mapper_nav_event_add_map")
64
+ def add_map_nav_item(sender, request: HttpRequest, **kwargs):
65
+ """
66
+ Adds a navigation item for the Sales Map to the event control panel sidebar.
67
+ """
68
+ has_permission = request.user.has_event_permission(request.organizer, request.event, REQUIRED_MAP_PERMISSION,
69
+ request=request)
70
+ if not has_permission: return []
71
+ try:
72
+ map_url = reverse(MAP_VIEW_URL_NAME, kwargs={
73
+ 'organizer': request.organizer.slug,
74
+ 'event': request.event.slug,
75
+ })
76
+ except NoReverseMatch:
77
+ logger.error(f"Could not reverse URL for map view '{MAP_VIEW_URL_NAME}'. Check urls.py.")
78
+ return []
79
+ is_active = False
80
+ if hasattr(request, 'resolver_match') and request.resolver_match:
81
+ is_active = request.resolver_match.view_name == MAP_VIEW_URL_NAME
82
+ return [{
83
+ 'label': _('Sales Map'),
84
+ 'url': map_url,
85
+ 'active': is_active,
86
+ 'icon': 'map-o',
87
+ }]
@@ -0,0 +1,91 @@
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 ---
8
+ from pretix.base.models import Order
9
+
10
+ # --- Import your Geocode model and geocoding functions ---
11
+ from .models import OrderGeocodeData
12
+ from .geocoding import (
13
+ get_formatted_address_from_order,
14
+ geocode_address,
15
+ DEFAULT_NOMINATIM_USER_AGENT # Import default for safety/logging
16
+ )
17
+
18
+ logger = logging.getLogger(__name__)
19
+
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
27
+ """
28
+ Celery task to geocode the address for a given order PK.
29
+ Accepts the Nominatim User-Agent as an argument.
30
+ """
31
+ try:
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)
37
+ logger.info(f"Starting geocoding task for Order {order.code} (PK: {order_pk})")
38
+
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:
43
+ logger.info(f"Geocode data already exists for Order {order.code}. Skipping.")
44
+ return # Exit successfully
45
+
46
+ # 1. Get formatted address string
47
+ address_str = get_formatted_address_from_order(order)
48
+ if not address_str:
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
57
+
58
+ # 2. Perform geocoding, passing the user agent received by the task
59
+ logger.debug(f"Attempting to geocode address for Order {order.code}: '{address_str}'")
60
+ coordinates = geocode_address(address_str, nominatim_user_agent=nominatim_user_agent)
61
+
62
+ # 3. Store result (or null if failed) using atomic transaction
63
+ with transaction.atomic():
64
+ if coordinates:
65
+ latitude, longitude = coordinates
66
+ obj, created = OrderGeocodeData.objects.update_or_create(
67
+ order=order,
68
+ defaults={'latitude': latitude, 'longitude': longitude}
69
+ )
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.")
83
+
84
+ except ObjectDoesNotExist: # More specific exception
85
+ logger.error(f"Order with PK {order_pk} not found in geocode_order_task.")
86
+ # Don't retry if the order doesn't exist
87
+ except Exception as e:
88
+ # Catch any other unexpected errors
89
+ logger.exception(f"Unexpected error in geocode_order_task for Order PK {order_pk}: {e}")
90
+ # Retry on potentially temporary errors (database, network issues etc.)
91
+ raise self.retry(exc=e) # Let Celery handle retry logic
@@ -1 +0,0 @@
1
- __version__ = "0.0.4"
@@ -1,113 +0,0 @@
1
- import logging
2
-
3
- # --- Import Django settings ---
4
- from django.conf import settings
5
- from geopy.exc import GeocoderServiceError, GeocoderTimedOut
6
- from geopy.geocoders import Nominatim
7
- from time import sleep
8
-
9
- # Configure logging for your plugin
10
- logger = logging.getLogger(__name__)
11
-
12
- # --- Configuration Default ---
13
- # Define a default/fallback User-Agent. Users *should* override this in pretix.cfg.
14
- DEFAULT_NOMINATIM_USER_AGENT = "pretix-map-plugin/unknown (Please configure nominatim_user_agent in pretix.cfg)"
15
-
16
-
17
- # --- Geocoding Function ---
18
-
19
- def geocode_address(address_string: str) -> tuple[float, float] | None:
20
- """
21
- Tries to geocode a given address string using Nominatim, reading the
22
- User-Agent from Pretix configuration.
23
-
24
- Args:
25
- address_string: A single string representing the address.
26
-
27
- Returns:
28
- A tuple (latitude, longitude) if successful, otherwise None.
29
- """
30
- # --- Get User-Agent from Pretix Settings ---
31
- # Access plugin settings via settings.plugins.<your_plugin_name>
32
- # The .get() method allows providing a default value if the setting is missing.
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.
40
- if user_agent == DEFAULT_NOMINATIM_USER_AGENT:
41
- logger.warning(
42
- "Using default Nominatim User-Agent. Please set a specific "
43
- "'nominatim_user_agent' under [pretix_mapplugin] in your "
44
- "pretix.cfg according to Nominatim's usage policy."
45
- )
46
- # --- End Settings Retrieval ---
47
-
48
- # Initialize the geolocator with the configured or default user_agent
49
- geolocator = Nominatim(user_agent=user_agent)
50
-
51
- try:
52
- # Add a 1-second delay to respect Nominatim's usage policy (1 req/sec)
53
- sleep(1)
54
-
55
- location = geolocator.geocode(address_string, timeout=10)
56
-
57
- if location:
58
- logger.debug(
59
- f"Geocoded '{address_string}' to ({location.latitude}, {location.longitude}) using User-Agent: {user_agent}")
60
- return (location.latitude, location.longitude)
61
- else:
62
- logger.warning(f"Could not geocode address: {address_string} (Address not found by Nominatim)")
63
- return None
64
-
65
- except GeocoderTimedOut:
66
- logger.error(f"Geocoding timed out for address: {address_string}")
67
- return None
68
- except GeocoderServiceError as e:
69
- logger.error(f"Geocoding service error for address '{address_string}': {e}")
70
- return None
71
- except Exception as e:
72
- logger.exception(f"An unexpected error occurred during geocoding for address '{address_string}': {e}")
73
- return None
74
-
75
-
76
- # --- Helper to Format Address from Pretix Order (No changes needed here) ---
77
-
78
- def get_formatted_address_from_order(order) -> str | None:
79
- """
80
- 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
-
94
-
95
- # --- Example Usage (Conceptual - No changes needed here) ---
96
- # This function itself isn't called directly, the logic is in tasks.py
97
- def process_order_for_geocoding(order):
98
- """Conceptual function showing how to use the geocoding."""
99
- address_str = get_formatted_address_from_order(order)
100
- if not address_str:
101
- logger.info(f"Order {order.code} has no invoice address to geocode.")
102
- return None
103
-
104
- coordinates = geocode_address(address_str) # This now uses the configured User-Agent
105
-
106
- if coordinates:
107
- latitude, longitude = coordinates
108
- logger.info(f"Successfully geocoded Order {order.code}: ({latitude}, {longitude})")
109
- # Store coordinates...
110
- return coordinates
111
- else:
112
- logger.warning(f"Failed to geocode Order {order.code} with address: {address_str}")
113
- return None
@@ -1,73 +0,0 @@
1
- import logging
2
- from django.dispatch import receiver
3
- from django.http import HttpRequest # For type hinting
4
- from django.urls import NoReverseMatch, reverse # Import reverse and NoReverseMatch
5
- from django.utils.translation import gettext_lazy as _ # For translatable labels
6
-
7
- # --- Pretix Signals ---
8
- from pretix.base.signals import order_paid
9
- from pretix.control.signals import nav_event # Import the navigation signal
10
-
11
- # --- Tasks ---
12
- from .tasks import geocode_order_task
13
-
14
- logger = logging.getLogger(__name__)
15
-
16
- # --- Constants ---
17
- MAP_VIEW_URL_NAME = 'plugins:pretix_mapplugin:event.settings.salesmap.show'
18
- # Define the permission required to see the map link
19
- REQUIRED_MAP_PERMISSION = 'can_view_orders'
20
-
21
-
22
- # --- Signal Receiver for Geocoding (Keep As Is) ---
23
- @receiver(order_paid, dispatch_uid="sales_mapper_order_paid_geocode")
24
- def trigger_geocoding_on_payment(sender, order, **kwargs):
25
- # ... (keep your existing geocoding logic) ...
26
- try:
27
- geocode_order_task.apply_async(args=[order.pk])
28
- logger.info(f"Geocoding task queued for paid order {order.code} (PK: {order.pk}).")
29
- except NameError:
30
- logger.error("geocode_order_task not found. Make sure it's imported correctly.")
31
- except Exception as e:
32
- logger.exception(f"Failed to queue geocoding task for order {order.code}: {e}")
33
-
34
-
35
- # --- Signal Receiver for Adding Navigation Item ---
36
- @receiver(nav_event, dispatch_uid="sales_mapper_nav_event_add_map")
37
- def add_map_nav_item(sender, request: HttpRequest, **kwargs):
38
- """
39
- Adds a navigation item for the Sales Map to the event control panel sidebar.
40
- """
41
- # Check if the user has the required permission for the current event
42
- has_permission = request.user.has_event_permission(
43
- request.organizer, request.event, REQUIRED_MAP_PERMISSION, request=request
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
49
- try:
50
- map_url = reverse(MAP_VIEW_URL_NAME, kwargs={
51
- 'organizer': request.organizer.slug,
52
- 'event': request.event.slug,
53
- })
54
- except NoReverseMatch:
55
- logger.error(f"Could not reverse URL for map view '{MAP_VIEW_URL_NAME}'. Check urls.py.")
56
- return [] # Return empty list if URL cannot be generated
57
-
58
- # Check if the current page *is* the map page to set the 'active' state
59
- is_active = False
60
- if hasattr(request, 'resolver_match') and request.resolver_match:
61
- is_active = request.resolver_match.view_name == MAP_VIEW_URL_NAME
62
-
63
- # Define the navigation item dictionary
64
- nav_item = {
65
- 'label': _('Sales Map'), # Translatable label
66
- 'url': map_url,
67
- 'active': is_active,
68
- 'icon': 'map-o', # Font Awesome icon name (fa-map-o) - adjust if needed
69
- # 'category': _('Orders'), # Optional: Suggests category, placement might vary
70
- }
71
-
72
- # Return the item in a list
73
- return [nav_item]
@@ -1,74 +0,0 @@
1
- import logging
2
- from pretix.base.models import Order
3
- from pretix.celery_app import app # Import the Pretix Celery app instance
4
-
5
- from .geocoding import ( # Import from Step 3
6
- geocode_address,
7
- get_formatted_address_from_order,
8
- )
9
- from .models import OrderGeocodeData
10
-
11
- logger = logging.getLogger(__name__)
12
-
13
- @app.task(bind=True, max_retries=3, default_retry_delay=60) # Configure retry behavior
14
- def geocode_order_task(self, order_pk: int):
15
- """
16
- Celery task to geocode an order's address and store the result.
17
- """
18
- try:
19
- order = Order.objects.select_related('invoice_address').get(pk=order_pk)
20
- logger.info(f"Starting geocoding task for Order {order.code} (PK: {order_pk})")
21
-
22
- # Check if already geocoded to prevent redundant work (e.g., if task retries)
23
- if OrderGeocodeData.objects.filter(order=order).exists():
24
- logger.info(f"Geocode data already exists for Order {order.code}. Skipping.")
25
- return # Exit successfully
26
-
27
- # 1. Get formatted address
28
- address_str = get_formatted_address_from_order(order)
29
- if not address_str:
30
- logger.warning(f"Order {order.code} has no invoice address to geocode.")
31
- return # Exit successfully, nothing to do
32
-
33
- # 2. Perform geocoding (using function from Step 3)
34
- logger.debug(f"Attempting to geocode address for Order {order.code}: '{address_str}'")
35
- coordinates = geocode_address(address_str) # This handles its own errors/logging
36
-
37
- # 3. Store result if successful
38
- if coordinates:
39
- latitude, longitude = coordinates
40
- try:
41
- # Use update_or_create to handle potential race conditions gracefully,
42
- # although the initial check makes it less likely.
43
- obj, created = OrderGeocodeData.objects.update_or_create(
44
- order=order,
45
- defaults={
46
- 'latitude': latitude,
47
- 'longitude': longitude
48
- }
49
- )
50
- if created:
51
- logger.info(f"Successfully geocoded and stored coordinates for Order {order.code}: ({latitude}, {longitude})")
52
- else:
53
- logger.info(f"Successfully geocoded and updated coordinates for Order {order.code}: ({latitude}, {longitude})")
54
-
55
- except Exception as e:
56
- logger.exception(f"Failed to save geocode data for Order {order.code} to database: {e}")
57
- # Optionally retry the task if saving failed
58
- self.retry(exc=e)
59
- else:
60
- # Geocoding function failed (already logged within geocode_address)
61
- logger.warning(f"Geocoding failed for Order {order.code}. No coordinates stored.")
62
- # Decide if you want to retry here based on the type of geocoding failure
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.
66
-
67
- except Order.DoesNotExist:
68
- logger.error(f"Order with PK {order_pk} not found for geocoding task.")
69
- # Don't retry if the order doesn't exist
70
- except Exception as e:
71
- # Catch any other unexpected errors in the task
72
- logger.exception(f"Unexpected error in geocode_order_task for Order PK {order_pk}: {e}")
73
- # Retry on unexpected errors
74
- self.retry(exc=e)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes