pretix-map 0.1.0__py3-none-any.whl → 0.1.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pretix-map
3
- Version: 0.1.0
3
+ Version: 0.1.2
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>
@@ -140,10 +140,12 @@ This command is essential for processing orders that were placed *before* the ma
140
140
  **Example Workflow:**
141
141
 
142
142
  1. **Test with Dry Run (All Organizers):**
143
+
143
144
  .. code-block:: bash
144
145
 
145
146
  python manage.py geocode_existing_orders --dry-run
146
147
  2. **(If satisfied) Run for Real (All Organizers):**
148
+
147
149
  .. code-block:: bash
148
150
 
149
151
  python manage.py geocode_existing_orders
@@ -1,10 +1,10 @@
1
- pretix_map-0.1.0.dist-info/licenses/LICENSE,sha256=RhQ89ePNDClBzEROahhwjDrBSEb5Zpx6XewZfGlY4Ss,569
2
- pretix_mapplugin/__init__.py,sha256=QTYqXqSTHFRkM9TEgpDFcHvwLbvqHDqvqfQ9EiXkcAM,23
1
+ pretix_map-0.1.2.dist-info/licenses/LICENSE,sha256=RhQ89ePNDClBzEROahhwjDrBSEb5Zpx6XewZfGlY4Ss,569
2
+ pretix_mapplugin/__init__.py,sha256=O1y2i1C4m602NbHgLdHmpflu9TGf21O6bs3ZEn_x0w4,23
3
3
  pretix_mapplugin/apps.py,sha256=AnThwyRw2AAz5f-kmXZ8hm85OmKnlDkRosVoQOBgPzE,830
4
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=maBMMSq5M7diy_EaNgKr8KRfGFj0U437u-MEl2NVYBw,3661
7
- pretix_mapplugin/tasks.py,sha256=tNSjm12IVN-NqyCG9AxN5jHsGRbMA13X7qMfkW30_qM,4518
6
+ pretix_mapplugin/signals.py,sha256=pSkucUPU6XgR0KLw4bKYEUW2Bqs5vfQXhO5YILQ2wps,3928
7
+ pretix_mapplugin/tasks.py,sha256=F_c36RwyTQzUJ9kBBos_-2zth1UXw_kpcQpUcE90BNM,5428
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,7 +14,7 @@ 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=QZGWXZfk-r3qffS3ernnOEsCK3feurc7hBpvCxhwa4U,9416
17
+ pretix_mapplugin/management/commands/geocode_existing_orders.py,sha256=y6v7Oug0kw6cLAn0iJgaFGv6NYGP9f9K-zNavlyJq_8,11703
18
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
@@ -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.1.0.dist-info/METADATA,sha256=P_53hGKTlW_N8JsWYFXsWouU64cW70rxkY9XoJUveCY,9514
43
- pretix_map-0.1.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
44
- pretix_map-0.1.0.dist-info/entry_points.txt,sha256=C3NAjeZHoCekafkLMCJynPcABRTK8AUprtQv7sUNDZs,137
45
- pretix_map-0.1.0.dist-info/top_level.txt,sha256=CAtEnkgA73zE9Gadm5mjt1SpXHBPOS-QWP0dQVoNToE,17
46
- pretix_map-0.1.0.dist-info/RECORD,,
42
+ pretix_map-0.1.2.dist-info/METADATA,sha256=Qf2yvDEHWVnIs5rZNPeHDTLDD-LLyWuoUKhVtg5yj9c,9518
43
+ pretix_map-0.1.2.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
44
+ pretix_map-0.1.2.dist-info/entry_points.txt,sha256=C3NAjeZHoCekafkLMCJynPcABRTK8AUprtQv7sUNDZs,137
45
+ pretix_map-0.1.2.dist-info/top_level.txt,sha256=CAtEnkgA73zE9Gadm5mjt1SpXHBPOS-QWP0dQVoNToE,17
46
+ pretix_map-0.1.2.dist-info/RECORD,,
@@ -1 +1 @@
1
- __version__ = "0.1.0"
1
+ __version__ = "0.1.2"
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  from django.core.management.base import BaseCommand, CommandError
3
- from django.core.exceptions import FieldDoesNotExist
3
+ from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist
4
4
 
5
5
  # --- Import Pretix Global Settings accessor ---
6
6
  from django_scopes import scope
@@ -8,6 +8,7 @@ from django_scopes import scope
8
8
  # Check if Pretix version uses AbstractSettingsHolder or GlobalSettingsObject
9
9
  # Adjust import based on Pretix version if needed. Assume AbstractSettingsHolder for newer Pretix.
10
10
  try:
11
+ # Newer Pretix often uses this pattern via AbstractSettingsHolder
11
12
  from pretix.base.settings import GlobalSettingsObject as SettingsProxy
12
13
  except ImportError:
13
14
  try:
@@ -21,7 +22,11 @@ except ImportError:
21
22
  except ImportError:
22
23
  # Fallback or raise error if neither is found
23
24
  logger.error("Could not determine Pretix settings accessor for management command.")
24
- raise ImportError("Cannot find Pretix settings accessor.")
25
+
26
+
27
+ # This will likely cause the command to fail later, but allows it to start
28
+ class SettingsProxy:
29
+ def __init__(self): self.settings = {} # Empty dict to avoid errors later
25
30
 
26
31
  # --- Import necessary Pretix models ---
27
32
  from pretix.base.models import Order, Event, Organizer
@@ -40,16 +45,23 @@ class Command(BaseCommand):
40
45
 
41
46
  def add_arguments(self, parser):
42
47
  parser.add_argument(
43
- '--organizer', type=str, help='Slug of a specific organizer to process orders for.',
48
+ '--organizer',
49
+ type=str,
50
+ help='Slug of a specific organizer to process orders for.',
44
51
  )
45
52
  parser.add_argument(
46
- '--event', type=str, help='Slug of a specific event to process orders for. Requires --organizer.',
53
+ '--event',
54
+ type=str,
55
+ help='Slug of a specific event to process orders for. Requires --organizer.',
47
56
  )
48
57
  parser.add_argument(
49
- '--dry-run', action='store_true', help='Simulate the process without actually queuing tasks.',
58
+ '--dry-run',
59
+ action='store_true',
60
+ help='Simulate the process without actually queuing tasks.',
50
61
  )
51
62
  parser.add_argument(
52
- '--force-recode', action='store_true',
63
+ '--force-recode',
64
+ action='store_true',
53
65
  help='Queue geocoding even for orders that already have geocode data.',
54
66
  )
55
67
 
@@ -65,13 +77,12 @@ class Command(BaseCommand):
65
77
  # --- Read User-Agent using Pretix Settings accessor ---
66
78
  user_agent = DEFAULT_NOMINATIM_USER_AGENT
67
79
  try:
68
- # Get settings holder instance
69
80
  gs = SettingsProxy()
70
81
  # 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.
82
+ # The format 'plugin:plugin_name:setting_name' is common
73
83
  setting_key = 'plugin:pretix_mapplugin:nominatim_user_agent'
74
- user_agent = gs.settings.get(setting_key, DEFAULT_NOMINATIM_USER_AGENT)
84
+ # Use .get() which is safer for dictionaries possibly returned by load_config
85
+ user_agent = getattr(gs, 'settings', {}).get(setting_key, DEFAULT_NOMINATIM_USER_AGENT)
75
86
 
76
87
  if user_agent == DEFAULT_NOMINATIM_USER_AGENT:
77
88
  self.stdout.write(self.style.WARNING(
@@ -88,12 +99,14 @@ class Command(BaseCommand):
88
99
  organizers_to_process = []
89
100
  if organizer_slug:
90
101
  try:
102
+ # Fetch specific organizer (outside scope)
91
103
  organizer = Organizer.objects.get(slug=organizer_slug)
92
104
  organizers_to_process.append(organizer)
93
105
  self.stdout.write(f"Processing specified organizer: {organizer.name} ({organizer_slug})")
94
106
  except Organizer.DoesNotExist:
95
107
  raise CommandError(f"Organizer with slug '{organizer_slug}' not found.")
96
108
  else:
109
+ # Fetch all organizers (outside scope)
97
110
  organizers_to_process = list(Organizer.objects.all())
98
111
  self.stdout.write(f"Processing all {len(organizers_to_process)} organizers...")
99
112
 
@@ -105,9 +118,10 @@ class Command(BaseCommand):
105
118
  # --- Iterate through organizers and activate scope ---
106
119
  for organizer in organizers_to_process:
107
120
  self.stdout.write(f"\n--- Processing Organizer: {organizer.name} ({organizer.slug}) ---")
121
+ current_organizer_pk = organizer.pk # Get the PK needed for the task kwarg
108
122
 
109
123
  with scope(organizer=organizer):
110
- # --- Get orders ---
124
+ # --- Get orders within scope ---
111
125
  orders_qs = Order.objects.filter(status=Order.STATUS_PAID)
112
126
 
113
127
  # --- Filter by event if specified ---
@@ -119,74 +133,93 @@ class Command(BaseCommand):
119
133
  except Event.DoesNotExist:
120
134
  self.stderr.write(self.style.WARNING(
121
135
  f" Event '{event_slug}' not found for this organizer. Skipping event filter."))
122
- if organizer_slug and event_slug: continue
136
+ # If filtering by event and it's not found for this org, skip this org entirely
137
+ if organizer_slug and event_slug:
138
+ self.stdout.write(
139
+ f" Skipping organizer '{organizer.slug}' as specified event was not found.")
140
+ continue # Move to the next organizer
123
141
 
124
142
  # --- Filter orders needing geocoding ---
125
- relation_name = 'geocode_data' # Ensure this matches your model
143
+ relation_name = 'geocode_data' # Ensure this matches your model's related_name
126
144
  if not force_recode:
127
145
  try:
128
146
  Order._meta.get_field(relation_name) # Check existence
147
+ # Filter orders that don't have the related geocode entry
129
148
  orders_to_process_qs = orders_qs.filter(**{f'{relation_name}__isnull': True})
130
149
  self.stdout.write(" Selecting paid orders missing geocode data...")
131
150
  except FieldDoesNotExist:
132
- self.stderr.write(
133
- self.style.ERROR(f" Relation '{relation_name}' not found. Skipping organizer."))
134
- continue
151
+ # This indicates a code/model setup error, stop for this org
152
+ self.stderr.write(self.style.ERROR(
153
+ f" Configuration Error: Reverse relation '{relation_name}' not found on Order model. Check OrderGeocodeData model definition (related_name). Skipping organizer '{organizer.slug}'."))
154
+ continue # Skip this organizer
135
155
  except Exception as e:
136
- self.stderr.write(self.style.ERROR(f" Error checking relation: {e}. Skipping organizer."))
137
- continue
156
+ # Catch other potential errors during query construction
157
+ self.stderr.write(self.style.ERROR(
158
+ f" Error checking relation '{relation_name}': {e}. Skipping organizer '{organizer.slug}'."))
159
+ continue # Skip this organizer
138
160
  else:
161
+ # If forcing, process all orders that matched the initial filters (paid, event)
139
162
  orders_to_process_qs = orders_qs
140
- self.stdout.write(self.style.WARNING(" Processing ALL selected paid orders (--force-recode)..."))
163
+ self.stdout.write(
164
+ self.style.WARNING(" Processing ALL selected paid orders (--force-recode specified)..."))
141
165
 
142
166
  # --- Process orders for this scope ---
143
- current_org_orders_count = orders_to_process_qs.count()
144
- all_checked_for_org = orders_qs.count()
145
- total_processed_orders += all_checked_for_org
167
+ current_org_orders_count = orders_to_process_qs.count() # Count orders to queue
168
+ all_checked_for_org = orders_qs.count() # Count all orders checked for this filter set
169
+ total_processed_orders += all_checked_for_org # Add to overall total
146
170
 
147
171
  if current_org_orders_count == 0:
148
172
  self.stdout.write(f" No orders need geocoding ({all_checked_for_org} checked).")
149
- continue
173
+ continue # Skip to next organizer
150
174
 
151
175
  self.stdout.write(
152
176
  f" Found {current_org_orders_count} order(s) to potentially geocode ({all_checked_for_org} checked).")
153
177
  org_queued = 0
154
178
  org_skipped = 0
155
179
 
156
- for order in orders_to_process_qs.iterator():
180
+ # Iterate and queue tasks
181
+ for order in orders_to_process_qs.iterator(): # Use iterator for memory efficiency
157
182
  if dry_run:
183
+ # Provide slightly more info in dry run
158
184
  self.stdout.write(
159
- f" [DRY RUN] Would queue Order: {order.code} (PK: {order.pk}) Event: {order.event.slug}")
185
+ f" [DRY RUN] Would queue Order: {order.code} (PK: {order.pk}, Org PK: {current_organizer_pk}) Event: {order.event.slug}")
160
186
  org_queued += 1
161
187
  else:
162
188
  try:
163
- # --- Pass user_agent to task ---
189
+ # --- Pass user_agent AND organizer_pk to the task ---
164
190
  geocode_order_task.apply_async(
165
- args=[order.pk],
166
- kwargs={'nominatim_user_agent': user_agent} # Pass as kwarg
191
+ args=[order.pk], # Positional arg is order_pk
192
+ kwargs={
193
+ 'nominatim_user_agent': user_agent,
194
+ 'organizer_pk': current_organizer_pk # Pass organizer PK
195
+ }
167
196
  )
197
+ # Don't log every single queue success unless verbose requested
168
198
  org_queued += 1
169
199
  except Exception as e:
170
- self.stderr.write(self.style.ERROR(f" ERROR queuing Order {order.code}: {e}"))
200
+ # Log error if queueing fails
201
+ self.stderr.write(self.style.ERROR(
202
+ f" ERROR queuing task for Order {order.code} (PK: {order.pk}): {e}"))
171
203
  logger.exception(f"Failed to queue geocoding task via command for order {order.code}: {e}")
172
204
  org_skipped += 1
173
205
 
174
- self.stdout.write(f" Queued: {org_queued}, Skipped: {org_skipped} for this organizer.")
206
+ # Report summary for the current organizer
207
+ self.stdout.write(f" Finished Organizer: Queued: {org_queued}, Skipped: {org_skipped}.")
175
208
  total_queued += org_queued
176
209
  total_skipped += org_skipped
177
- # End scope
210
+ # End scope 'with' block
178
211
 
179
- # --- Final Report ---
212
+ # --- Final Overall Report ---
180
213
  self.stdout.write("=" * 40)
181
214
  self.stdout.write("Overall Summary:")
182
215
  self.stdout.write(f" Organizers processed: {len(organizers_to_process)}")
183
- self.stdout.write(f" Total orders checked (paid): {total_processed_orders}")
216
+ self.stdout.write(f" Total orders checked (paid, matching filters): {total_processed_orders}")
184
217
  if dry_run:
185
218
  self.stdout.write(
186
219
  self.style.SUCCESS(f"[DRY RUN] Complete. Would have queued tasks for {total_queued} order(s)."))
187
220
  else:
188
- self.stdout.write(self.style.SUCCESS(f"Complete. Queued tasks for {total_queued} order(s)."))
221
+ self.stdout.write(self.style.SUCCESS(f"Complete. Successfully queued tasks for {total_queued} order(s)."))
189
222
  if total_skipped > 0:
190
- self.stdout.write(
191
- self.style.WARNING(f"Skipped {total_skipped} order(s) total due to errors during queueing."))
223
+ self.stdout.write(self.style.WARNING(
224
+ f"Skipped {total_skipped} order(s) total due to errors during queueing (check logs)."))
192
225
  self.stdout.write("=" * 40)
@@ -3,7 +3,6 @@ from django.dispatch import receiver
3
3
  from django.urls import reverse, NoReverseMatch
4
4
  from django.utils.translation import gettext_lazy as _
5
5
  from django.http import HttpRequest
6
- # Import Django settings to read config in web process
7
6
  from django.conf import settings
8
7
 
9
8
  # --- Pretix Signals ---
@@ -13,50 +12,56 @@ from pretix.control.signals import nav_event
13
12
  # --- Tasks ---
14
13
  from .tasks import geocode_order_task
15
14
  # --- Geocoding Default ---
16
- from .geocoding import DEFAULT_NOMINATIM_USER_AGENT # Import default
15
+ from .geocoding import DEFAULT_NOMINATIM_USER_AGENT
17
16
 
18
17
  logger = logging.getLogger(__name__)
19
18
 
20
19
  # --- Constants ---
21
20
  MAP_VIEW_URL_NAME = 'plugins:pretix_mapplugin:event.settings.salesmap.show'
22
21
  REQUIRED_MAP_PERMISSION = 'can_view_orders'
23
- PLUGIN_NAME = 'pretix_mapplugin' # Define plugin name for settings access
22
+ PLUGIN_NAME = 'pretix_mapplugin'
24
23
 
25
24
 
26
- # --- Signal Receiver for Geocoding (Reads setting, passes to task) ---
25
+ # --- Signal Receiver for Geocoding (Passes organizer_pk) ---
27
26
  @receiver(order_paid, dispatch_uid="sales_mapper_order_paid_geocode")
28
27
  def trigger_geocoding_on_payment(sender, order, **kwargs):
29
28
  """
30
29
  Listens for the order_paid signal, reads geocoding config,
31
- and queues the geocoding task with the config.
30
+ and queues the geocoding task with order_pk, organizer_pk, and config.
32
31
  """
33
- user_agent = DEFAULT_NOMINATIM_USER_AGENT # Start with default
32
+ user_agent = DEFAULT_NOMINATIM_USER_AGENT
33
+ organizer_pk = None # Initialize
34
34
  try:
35
- # --- Read User-Agent from settings (works in web process) ---
36
- # Check structure defensively before accessing
35
+ # Ensure order has event and organizer before proceeding
36
+ if not order or not order.event or not order.event.organizer:
37
+ logger.error(f"Order {order.code} is missing event or organizer information. Cannot queue task.")
38
+ return
39
+
40
+ organizer_pk = order.event.organizer.pk # Get organizer PK
41
+
42
+ # --- Read User-Agent from settings ---
37
43
  if hasattr(settings, 'plugins') and hasattr(settings.plugins, PLUGIN_NAME):
38
44
  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
- )
45
+ user_agent = plugin_settings.get('nominatim_user_agent', DEFAULT_NOMINATIM_USER_AGENT)
43
46
  else:
44
- logger.warning(
45
- f"Could not access settings.plugins.{PLUGIN_NAME}, "
46
- "using default Nominatim User-Agent for task."
47
- )
47
+ logger.warning(f"Could not access settings.plugins.{PLUGIN_NAME}, using default User-Agent.")
48
48
 
49
- # --- Queue task with user_agent as keyword argument ---
49
+ # --- Queue task with user_agent and organizer_pk as keyword arguments ---
50
50
  geocode_order_task.apply_async(
51
- args=[order.pk],
52
- kwargs={'nominatim_user_agent': user_agent} # Pass as kwarg
51
+ args=[order.pk], # Keep order_pk as positional argument
52
+ kwargs={
53
+ 'nominatim_user_agent': user_agent,
54
+ 'organizer_pk': organizer_pk # Pass organizer PK
55
+ }
53
56
  )
54
- logger.info(f"Geocoding task queued for paid order {order.code} (PK: {order.pk}).")
57
+ logger.info(f"Geocoding task queued for paid order {order.code} (PK: {order.pk}, Org PK: {organizer_pk}).")
55
58
 
56
- except ImportError: # Error finding geocode_order_task itself if tasks.py fails
59
+ except ImportError:
57
60
  logger.exception("Could not import geocode_order_task. Check tasks.py.")
58
61
  except Exception as e:
59
- logger.exception(f"Failed to queue geocoding task for order {order.code}: {e}")
62
+ # Log the organizer PK as well if available
63
+ org_info = f" (Org PK: {organizer_pk})" if organizer_pk else ""
64
+ logger.exception(f"Failed to queue geocoding task for order {order.code}{org_info}: {e}")
60
65
 
61
66
 
62
67
  # --- Signal Receiver for Adding Navigation Item (No changes needed) ---
pretix_mapplugin/tasks.py CHANGED
@@ -2,89 +2,112 @@ import logging
2
2
  from django.db import transaction
3
3
  from django.core.exceptions import ObjectDoesNotExist
4
4
 
5
+ # --- Import django-scopes ---
6
+ from django_scopes import scope
7
+
5
8
  # --- Use Pretix Celery app instance ---
6
9
  from pretix.celery_app import app
7
10
  # --- Import necessary Pretix models ---
8
- from pretix.base.models import Order
11
+ from pretix.base.models import Order, Organizer # Import Organizer
9
12
 
10
13
  # --- Import your Geocode model and geocoding functions ---
11
14
  from .models import OrderGeocodeData
12
15
  from .geocoding import (
13
16
  get_formatted_address_from_order,
14
17
  geocode_address,
15
- DEFAULT_NOMINATIM_USER_AGENT # Import default for safety/logging
18
+ DEFAULT_NOMINATIM_USER_AGENT
16
19
  )
17
20
 
18
21
  logger = logging.getLogger(__name__)
19
22
 
20
23
 
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
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
25
+ # --- Accept organizer_pk as kwarg ---
26
+ def geocode_order_task(self, order_pk: int, organizer_pk: int | None = None, nominatim_user_agent: str | None = None):
27
27
  """
28
28
  Celery task to geocode the address for a given order PK.
29
- Accepts the Nominatim User-Agent as an argument.
29
+ Accepts organizer_pk and Nominatim User-Agent as arguments.
30
+ Fetches Organizer first, then activates scope.
30
31
  """
32
+ organizer = None
33
+ order = None
31
34
  try:
32
- # Fetch order with related address and country data efficiently
33
- order = Order.objects.select_related(
34
- 'invoice_address',
35
- ).get(pk=order_pk)
36
- logger.info(f"Starting geocoding task for Order {order.code} (PK: {order_pk})")
35
+ # --- Step 1: Fetch Organizer (unscoped) ---
36
+ if organizer_pk is None:
37
+ # This should ideally not happen if called correctly, but handle defensively
38
+ logger.error(f"organizer_pk not provided for Order PK {order_pk}. Cannot activate scope.")
39
+ # Depending on policy, you might retry, skip, or raise an error.
40
+ # Skipping for now.
41
+ return
37
42
 
38
- # Check if already geocoded to prevent redundant work
39
- # Replace 'geocode_data' if your related_name is different
40
- relation_name = 'geocode_data' # Ensure this matches your OrderGeocodeData.order related_name
41
- if hasattr(order, relation_name) and getattr(order, relation_name) is not None:
42
- logger.info(f"Geocode data already exists for Order {order.code}. Skipping.")
43
- return # Exit successfully
43
+ try:
44
+ organizer = Organizer.objects.get(pk=organizer_pk)
45
+ except ObjectDoesNotExist:
46
+ logger.error(f"Organizer with PK {organizer_pk} not found (for Order PK {order_pk}).")
47
+ # Don't retry if organizer doesn't exist
48
+ return
44
49
 
45
- # 1. Get formatted address string
46
- address_str = get_formatted_address_from_order(order)
47
- if not address_str:
48
- logger.info(f"Order {order.code} has no address suitable for geocoding. Storing null coordinates.")
49
- # Store null to prevent reprocessing
50
- with transaction.atomic():
51
- OrderGeocodeData.objects.update_or_create(
52
- order=order,
53
- defaults={'latitude': None, 'longitude': None}
54
- )
55
- return # Exit successfully, nothing to geocode
50
+ # --- Step 2: Activate Scope ---
51
+ with scope(organizer=organizer):
52
+ # --- Step 3: Fetch Order (now within scope) ---
53
+ try:
54
+ order = Order.objects.select_related(
55
+ 'invoice_address' # Only need this direct relation now
56
+ ).get(pk=order_pk)
57
+ except ObjectDoesNotExist:
58
+ logger.error(f"Order with PK {order_pk} not found within scope of Org {organizer_pk}.")
59
+ # Don't retry if order doesn't exist in this scope
60
+ return
61
+
62
+ logger.info(
63
+ f"Starting geocoding task for Order {order.code} (PK: {order_pk}) within scope of Organizer '{organizer.slug}'")
56
64
 
57
- # 2. Perform geocoding, passing the user agent received by the task
58
- logger.debug(f"Attempting to geocode address for Order {order.code}: '{address_str}'")
59
- coordinates = geocode_address(address_str, nominatim_user_agent=nominatim_user_agent)
65
+ # --- Rest of the logic runs within scope ---
66
+ relation_name = 'geocode_data'
67
+ if OrderGeocodeData.objects.filter(order_id=order_pk).exists():
68
+ logger.info(f"Geocode data already exists for Order {order.code} (checked within scope). Skipping.")
69
+ return
60
70
 
61
- # 3. Store result (or null if failed) using atomic transaction
62
- with transaction.atomic():
63
- if coordinates:
64
- latitude, longitude = coordinates
65
- obj, created = OrderGeocodeData.objects.update_or_create(
66
- order=order,
67
- defaults={'latitude': latitude, 'longitude': longitude}
68
- )
69
- log_level = logging.INFO if created else logging.DEBUG # Be less noisy on updates
70
- logger.log(log_level,
71
- f"Saved{' new' if created else ' updated'} geocode data for Order {order.code}: ({latitude}, {longitude})")
72
- else:
73
- logger.warning(f"Geocoding failed for Order {order.code}. Storing null coordinates.")
74
- # Store nulls to indicate an attempt was made and failed
75
- obj, created = OrderGeocodeData.objects.update_or_create(
76
- order=order,
77
- defaults={'latitude': None, 'longitude': None}
78
- )
79
- log_level = logging.INFO if created else logging.DEBUG
80
- logger.log(log_level,
81
- f"Saved{' new' if created else ' updated'} null geocode data for Order {order.code} after failed attempt.")
71
+ address_str = get_formatted_address_from_order(order)
72
+ if not address_str:
73
+ logger.info(f"Order {order.code} has no address suitable for geocoding. Storing null coordinates.")
74
+ with transaction.atomic():
75
+ OrderGeocodeData.objects.update_or_create(
76
+ order=order, defaults={'latitude': None, 'longitude': None}
77
+ )
78
+ return
79
+
80
+ logger.debug(f"Attempting to geocode address for Order {order.code}: '{address_str}'")
81
+ coordinates = geocode_address(address_str, nominatim_user_agent=nominatim_user_agent)
82
+
83
+ with transaction.atomic():
84
+ if coordinates:
85
+ latitude, longitude = coordinates
86
+ obj, created = OrderGeocodeData.objects.update_or_create(
87
+ order=order, defaults={'latitude': latitude, 'longitude': longitude}
88
+ )
89
+ log_level = logging.INFO if created else logging.DEBUG
90
+ logger.log(log_level,
91
+ f"Saved{' new' if created else ' updated'} geocode data for Order {order.code}: ({latitude}, {longitude})")
92
+ else:
93
+ logger.warning(f"Geocoding failed for Order {order.code}. Storing null coordinates.")
94
+ obj, created = OrderGeocodeData.objects.update_or_create(
95
+ order=order, defaults={'latitude': None, 'longitude': None}
96
+ )
97
+ log_level = logging.INFO if created else logging.DEBUG
98
+ logger.log(log_level,
99
+ f"Saved{' new' if created else ' updated'} null geocode data for Order {order.code} after failed attempt.")
100
+ # --- Scope deactivated automatically ---
82
101
 
83
- except ObjectDoesNotExist: # More specific exception
84
- logger.error(f"Order with PK {order_pk} not found in geocode_order_task.")
85
- # Don't retry if the order doesn't exist
102
+ # --- Outer exception handling ---
103
+ except ObjectDoesNotExist:
104
+ # Should be caught earlier now, but keep for safety
105
+ obj_type = "Organizer" if organizer is None else "Order"
106
+ obj_pk = organizer_pk if organizer is None else order_pk
107
+ logger.error(f"{obj_type} with PK {obj_pk} not found.")
86
108
  except Exception as e:
87
- # Catch any other unexpected errors
88
- logger.exception(f"Unexpected error in geocode_order_task for Order PK {order_pk}: {e}")
89
- # Retry on potentially temporary errors (database, network issues etc.)
90
- raise self.retry(exc=e) # Let Celery handle retry logic
109
+ org_info = f" (Org PK: {organizer_pk})" if organizer_pk else ""
110
+ order_info = f" (Order PK: {order_pk})" if order_pk else ""
111
+ logger.exception(f"Unexpected error in geocode_order_task{org_info}{order_info}: {e}")
112
+ # Retry on potentially temporary errors
113
+ raise self.retry(exc=e)