pretix-map 0.1.3__py3-none-any.whl → 0.1.5__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.
Files changed (33) hide show
  1. pretix_map-0.1.5.dist-info/METADATA +88 -0
  2. {pretix_map-0.1.3.dist-info → pretix_map-0.1.5.dist-info}/RECORD +32 -30
  3. {pretix_map-0.1.3.dist-info → pretix_map-0.1.5.dist-info}/WHEEL +1 -1
  4. {pretix_map-0.1.3.dist-info → pretix_map-0.1.5.dist-info}/licenses/LICENSE +15 -15
  5. pretix_mapplugin/__init__.py +1 -1
  6. pretix_mapplugin/apps.py +28 -28
  7. pretix_mapplugin/geocoding.py +162 -102
  8. pretix_mapplugin/locale/de/LC_MESSAGES/django.po +12 -12
  9. pretix_mapplugin/locale/de_Informal/LC_MESSAGES/django.po +12 -12
  10. pretix_mapplugin/management/commands/geocode_existing_orders.py +271 -271
  11. pretix_mapplugin/migrations/0001_initial.py +27 -27
  12. pretix_mapplugin/migrations/0002_remove_ordergeocodedata_geocoded_timestamp_and_more.py +32 -32
  13. pretix_mapplugin/migrations/0003_mapmilestone.py +27 -0
  14. pretix_mapplugin/models.py +71 -47
  15. pretix_mapplugin/signals.py +77 -92
  16. pretix_mapplugin/static/pretix_mapplugin/css/salesmap.css +51 -51
  17. pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js +342 -442
  18. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.Default.css +59 -59
  19. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/MarkerCluster.css +14 -14
  20. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-heat.js +10 -10
  21. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.esm.js +14419 -14419
  22. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet-src.js +14512 -14512
  23. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.css +661 -661
  24. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.js +5 -5
  25. pretix_mapplugin/static/pretix_mapplugin/libs/leaflet-sales-map/leaflet.markercluster.js +2 -2
  26. pretix_mapplugin/tasks.py +144 -113
  27. pretix_mapplugin/templates/pretix_mapplugin/map_page.html +154 -68
  28. pretix_mapplugin/templates/pretix_mapplugin/milestones.html +53 -0
  29. pretix_mapplugin/urls.py +38 -21
  30. pretix_mapplugin/views.py +272 -163
  31. pretix_map-0.1.3.dist-info/METADATA +0 -195
  32. {pretix_map-0.1.3.dist-info → pretix_map-0.1.5.dist-info}/entry_points.txt +0 -0
  33. {pretix_map-0.1.3.dist-info → pretix_map-0.1.5.dist-info}/top_level.txt +0 -0
pretix_mapplugin/views.py CHANGED
@@ -1,163 +1,272 @@
1
- import logging
2
- from django.db.models import Prefetch # Needed for prefetch_related optimization
3
- from django.http import HttpResponse, JsonResponse # Import HttpResponse
4
-
5
- # --- CORRECTED IMPORTS ---
6
- from django.urls import reverse # Needed to generate URLs
7
- from django.utils.formats import date_format # For localized date formatting
8
- from django.utils.translation import gettext_lazy as _
9
- from django.views.generic import TemplateView, View
10
- from pretix.base.models import Order # Make sure Order is imported
11
- from pretix.control.views.event import EventSettingsViewMixin
12
-
13
- from .models import OrderGeocodeData
14
-
15
- # --- END CORRECTED IMPORTS ---
16
-
17
-
18
- # --- Import CSP helpers (Still needed for SalesMapView) ---
19
- try:
20
- from pretix.base.csp import _merge_csp, _parse_csp, _render_csp
21
- except ImportError:
22
- from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
23
-
24
- logger = logging.getLogger(__name__)
25
-
26
-
27
- # --- SalesMapDataView (Modified to provide more data) ---
28
- class SalesMapDataView(EventSettingsViewMixin, View):
29
- permission = 'can_view_orders'
30
-
31
- def get(self, request, *args, **kwargs):
32
- event = self.request.event
33
- organizer = request.organizer # Get organizer for URL generation
34
-
35
- locations_data = [] # Initialize list to hold data for JSON
36
-
37
- try:
38
- # Fetch geocode entries, prefetch related order and its positions
39
- geocode_entries = OrderGeocodeData.objects.filter(
40
- order__event=event,
41
- latitude__isnull=False,
42
- longitude__isnull=False
43
- ).select_related(
44
- 'order' # Select the direct foreign key
45
- ).prefetch_related(
46
- Prefetch('order__positions') # Use Prefetch for better control if needed, or just 'order__positions'
47
- )
48
-
49
- for entry in geocode_entries:
50
- order = entry.order
51
- order_url = None
52
- tooltip_parts = []
53
-
54
- # 1. Generate Order URL
55
- try:
56
- # Use the standard name for the control panel order detail view
57
- # Ensure 'control:event.order' is the correct name in your Pretix version
58
- order_url = reverse('control:event.order', kwargs={
59
- 'organizer': organizer.slug,
60
- 'event': event.slug,
61
- 'code': order.code,
62
- })
63
- except Exception as e:
64
- logger.warning(f"Could not reverse URL for order {order.code}: {e}")
65
-
66
- # 2. Build Tooltip String
67
- tooltip_parts.append(f"<strong>Order:</strong> {order.code}")
68
-
69
- # Format order date (using Django's localized formatting)
70
- try:
71
- formatted_date = date_format(order.datetime, format='SHORT_DATETIME_FORMAT', use_l10n=True)
72
- tooltip_parts.append(f"<strong>Date:</strong> {formatted_date}")
73
- except Exception as e:
74
- logger.warning(f"Could not format date for order {order.code}: {e}")
75
- tooltip_parts.append("<strong>Date:</strong> N/A") # Fallback
76
-
77
- # Count positions (tickets/items) - efficient due to prefetch_related
78
- position_count = order.positions.count() # count() is efficient on prefetched QuerySets
79
- tooltip_parts.append(f"<strong>Items:</strong> {position_count}")
80
-
81
- # Combine tooltip parts with HTML line breaks
82
- tooltip_string = "<br>".join(tooltip_parts)
83
-
84
- # 3. Append data to the list
85
- locations_data.append({
86
- "lat": entry.latitude,
87
- "lon": entry.longitude,
88
- "tooltip": tooltip_string, # The enhanced tooltip
89
- "order_url": order_url, # The URL for clicking
90
- })
91
-
92
- logger.debug(f"Returning {len(locations_data)} enriched coordinates for event {event.slug}")
93
- return JsonResponse({'locations': locations_data})
94
-
95
- except OrderGeocodeData.DoesNotExist:
96
- logger.info(f"No geocode data found for event {event.slug}")
97
- return JsonResponse({'locations': []})
98
- except Exception as e:
99
- logger.exception(f"Error retrieving or processing geocode data for event {event.slug}: {e}")
100
- # Provide a more generic error in production
101
- return JsonResponse({'error': _('Could not retrieve coordinate data due to a server error.')}, status=500)
102
-
103
-
104
- class SalesMapView(EventSettingsViewMixin, TemplateView):
105
- permission = 'can_view_orders'
106
- template_name = 'pretix_mapplugin/map_page.html'
107
-
108
- def get(self, request, *args, **kwargs):
109
- try:
110
- response = super().get(request, *args, **kwargs)
111
- except Exception as e:
112
- logger.exception(f"Error rendering template {self.template_name}: {e}")
113
- return HttpResponse(_("Error loading map page."), status=500)
114
-
115
- logger.debug(f"View: Attempting CSP modification for {request.path}")
116
-
117
- # 2. Get existing CSP header
118
- current_csp = {}
119
- header_key = 'Content-Security-Policy'
120
- if header_key in response:
121
- header_value = response[header_key]
122
- if isinstance(header_value, bytes):
123
- header_value = header_value.decode('utf-8')
124
- try:
125
- current_csp = _parse_csp(header_value)
126
- logger.debug(f"View: Found existing CSP header: {header_value}")
127
- except Exception as e:
128
- logger.error(f"View: Error parsing existing CSP header '{header_value}': {e}")
129
- current_csp = {}
130
- else:
131
- logger.debug("View: No existing CSP header found.")
132
- current_csp = {}
133
-
134
- # 3. Define additions: img-src AND style-src
135
- map_csp_additions = {
136
- 'img-src': [
137
- 'https://*.tile.openstreetmap.org',
138
- ],
139
- 'style-src': [
140
- "'unsafe-inline'", # Allow inline styles needed by Leaflet/plugins
141
- ]
142
- }
143
-
144
- # 4. Merge additions
145
- try:
146
- _merge_csp(current_csp, map_csp_additions)
147
- logger.debug(f"View: CSP dict after merge: {current_csp}")
148
- except Exception as e:
149
- logger.error(f"View: Error merging CSP additions: {e}")
150
-
151
- # 5. Render and set the header
152
- if current_csp:
153
- try:
154
- new_header_value = _render_csp(current_csp)
155
- response[header_key] = new_header_value
156
- logger.info(f"View: Setting/modifying CSP header to: {new_header_value}")
157
- except Exception as e:
158
- logger.error(f"View: Error rendering final CSP header: {e}")
159
- else:
160
- logger.warning("View: CSP dictionary is empty after merge, header not set.")
161
-
162
- # 6. Return the modified response object
163
- return response
1
+ import logging
2
+ from collections import Counter
3
+ from django.conf import settings
4
+ from django.contrib import messages
5
+ from django.db.models import Prefetch
6
+ from django.forms import inlineformset_factory
7
+ from django.http import HttpResponse, JsonResponse
8
+ from django.shortcuts import redirect
9
+ from django.urls import reverse
10
+ from django.utils.formats import date_format
11
+ from django.utils.translation import gettext_lazy as _
12
+ from django.views.generic import TemplateView, View
13
+
14
+ from pretix.base.models import Order, Event
15
+ from pretix.control.views.event import EventSettingsViewMixin
16
+
17
+ from .models import OrderGeocodeData, MapMilestone
18
+ from .tasks import geocode_order_task
19
+ from .geocoding import (
20
+ DEFAULT_NOMINATIM_USER_AGENT,
21
+ geocode_event_location,
22
+ get_best_address_string,
23
+ calculate_distance
24
+ )
25
+
26
+ # --- CSP Helpers ---
27
+ try:
28
+ from pretix.base.csp import _merge_csp, _parse_csp, _render_csp
29
+ except ImportError:
30
+ from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ # --- Milestone Management ---
36
+ MilestoneFormSet = inlineformset_factory(
37
+ Event,
38
+ MapMilestone,
39
+ fields=('date', 'label'),
40
+ extra=1,
41
+ can_delete=True
42
+ )
43
+
44
+
45
+ class MilestoneSettingsView(EventSettingsViewMixin, TemplateView):
46
+ permission = 'can_change_event_settings'
47
+ template_name = 'pretix_mapplugin/milestones.html'
48
+
49
+ def get_context_data(self, **kwargs):
50
+ ctx = super().get_context_data(**kwargs)
51
+ if 'formset' not in ctx:
52
+ ctx['formset'] = MilestoneFormSet(
53
+ instance=self.request.event,
54
+ queryset=MapMilestone.objects.filter(event=self.request.event)
55
+ )
56
+ return ctx
57
+
58
+ def post(self, request, *args, **kwargs):
59
+ formset = MilestoneFormSet(
60
+ request.POST,
61
+ instance=self.request.event,
62
+ queryset=MapMilestone.objects.filter(event=self.request.event)
63
+ )
64
+ if formset.is_valid():
65
+ formset.save()
66
+ return redirect(reverse('plugins:pretix_mapplugin:event.settings.salesmap.milestones', kwargs={
67
+ 'organizer': self.request.organizer.slug,
68
+ 'event': self.request.event.slug,
69
+ }))
70
+ return self.render_to_response(self.get_context_data(formset=formset))
71
+
72
+
73
+ class SingleGeocodeView(EventSettingsViewMixin, View):
74
+ permission = 'can_change_event_settings'
75
+
76
+ def post(self, request, *args, **kwargs):
77
+ order_pk = self.kwargs.get('order')
78
+ try:
79
+ order = Order.objects.get(pk=order_pk, event=self.request.event)
80
+ except Order.DoesNotExist:
81
+ return JsonResponse({'error': 'Order not found'}, status=404)
82
+
83
+ user_agent = DEFAULT_NOMINATIM_USER_AGENT
84
+ if hasattr(settings, 'CONFIG_FILE') and settings.CONFIG_FILE.has_section('pretix_mapplugin'):
85
+ user_agent = settings.CONFIG_FILE.get('pretix_mapplugin', 'nominatim_user_agent', fallback=DEFAULT_NOMINATIM_USER_AGENT)
86
+
87
+ geocode_order_task.apply_async(
88
+ args=[order.pk],
89
+ kwargs={
90
+ 'nominatim_user_agent': user_agent,
91
+ 'organizer_pk': self.request.organizer.pk
92
+ }
93
+ )
94
+ return JsonResponse({'success': True})
95
+
96
+
97
+ class TriggerGeocodingView(EventSettingsViewMixin, View):
98
+ permission = 'can_change_event_settings'
99
+
100
+ def post(self, request, *args, **kwargs):
101
+ user_agent = DEFAULT_NOMINATIM_USER_AGENT
102
+ if hasattr(settings, 'CONFIG_FILE') and settings.CONFIG_FILE.has_section('pretix_mapplugin'):
103
+ user_agent = settings.CONFIG_FILE.get('pretix_mapplugin', 'nominatim_user_agent', fallback=DEFAULT_NOMINATIM_USER_AGENT)
104
+
105
+ orders_to_geocode = Order.objects.filter(
106
+ event=self.request.event,
107
+ status__in=[Order.STATUS_PAID, Order.STATUS_PENDING]
108
+ ).exclude(
109
+ geocode_data__latitude__isnull=False
110
+ )
111
+
112
+ count = 0
113
+ for order in orders_to_geocode:
114
+ geocode_order_task.apply_async(
115
+ args=[order.pk],
116
+ kwargs={
117
+ 'nominatim_user_agent': user_agent,
118
+ 'organizer_pk': self.request.organizer.pk
119
+ }
120
+ )
121
+ count += 1
122
+
123
+ messages.success(request, _("Queued {} orders for geocoding.").format(count))
124
+ return redirect(reverse('plugins:pretix_mapplugin:event.settings.salesmap.show', kwargs={
125
+ 'organizer': self.request.organizer.slug,
126
+ 'event': self.request.event.slug,
127
+ }))
128
+
129
+
130
+ class SalesMapDataView(EventSettingsViewMixin, View):
131
+ permission = 'can_view_orders'
132
+
133
+ def get(self, request, *args, **kwargs):
134
+ event = self.request.event
135
+ organizer = request.organizer
136
+
137
+ user_agent = DEFAULT_NOMINATIM_USER_AGENT
138
+ if hasattr(settings, 'CONFIG_FILE') and settings.CONFIG_FILE.has_section('pretix_mapplugin'):
139
+ user_agent = settings.CONFIG_FILE.get('pretix_mapplugin', 'nominatim_user_agent', fallback=DEFAULT_NOMINATIM_USER_AGENT)
140
+
141
+ milestones = [
142
+ {'date': m.date.isoformat(), 'label': m.label}
143
+ for m in MapMilestone.objects.filter(event=event)
144
+ ]
145
+
146
+ locations_data = []
147
+ failed_orders_data = []
148
+ city_counter = Counter()
149
+ item_counter = Counter()
150
+ distances = []
151
+ total_revenue = 0
152
+
153
+ try:
154
+ event_marker = None
155
+ event_coords = None
156
+ if event.location:
157
+ event_coords = geocode_event_location(event, user_agent)
158
+ if event_coords:
159
+ event_marker = {'lat': event_coords[0], 'lon': event_coords[1], 'name': str(event.name), 'location': str(event.location)}
160
+
161
+ geocode_entries = OrderGeocodeData.objects.filter(
162
+ order__event=event, latitude__isnull=False, longitude__isnull=False
163
+ ).select_related('order', 'order__invoice_address').prefetch_related('order__positions__item')
164
+
165
+ for entry in geocode_entries:
166
+ order = entry.order
167
+ iso_date = order.datetime.isoformat() if order.datetime else ""
168
+ revenue = float(order.total)
169
+
170
+ status = 'pending'
171
+ if order.status == Order.STATUS_PAID: status = 'paid'
172
+ elif order.status == Order.STATUS_CANCELED: status = 'canceled'
173
+
174
+ if status != 'canceled':
175
+ total_revenue += revenue
176
+ if order.invoice_address and order.invoice_address.city:
177
+ city_counter[order.invoice_address.city] += 1
178
+ dist_km = calculate_distance(event_coords, (entry.latitude, entry.longitude)) if event_coords else None
179
+ if dist_km is not None: distances.append(dist_km)
180
+ else:
181
+ dist_km = calculate_distance(event_coords, (entry.latitude, entry.longitude)) if event_coords else None
182
+
183
+ item_names = [str(p.item.name) for p in order.positions.all() if p.item]
184
+ if status != 'canceled':
185
+ for name in item_names: item_counter[name] += 1
186
+
187
+ tooltip_parts = [f"<strong>Order:</strong> {order.code} ({status.upper()})"]
188
+ if order.datetime:
189
+ tooltip_parts.append(f"<strong>Date:</strong> {date_format(order.datetime, format='SHORT_DATETIME_FORMAT', use_l10n=True)}")
190
+ tooltip_parts.append(f"<strong>Items:</strong> {order.positions.count()}")
191
+ tooltip_parts.append(f"<strong>Total:</strong> {order.total} {event.currency}")
192
+ if dist_km: tooltip_parts.append(f"<strong>Dist:</strong> {dist_km:.1f} km")
193
+
194
+ locations_data.append({
195
+ "lat": entry.latitude, "lon": entry.longitude, "tooltip": "<br>".join(tooltip_parts),
196
+ "order_url": reverse('control:event.order', kwargs={'organizer': organizer.slug, 'event': event.slug, 'code': order.code}),
197
+ "items": item_names, "date": iso_date, "dist": dist_km, "revenue": revenue, "status": status
198
+ })
199
+
200
+ failed_entries = OrderGeocodeData.objects.filter(order__event=event, latitude__isnull=True).select_related('order', 'order__invoice_address')
201
+ for entry in failed_entries:
202
+ failed_orders_data.append({
203
+ 'pk': entry.order.pk, 'code': entry.order.code,
204
+ 'address': get_best_address_string(entry.order) or _("No address"),
205
+ 'url': reverse('control:event.order', kwargs={'organizer': organizer.slug, 'event': event.slug, 'code': entry.order.code}),
206
+ 'retry_url': reverse('plugins:pretix_mapplugin:event.settings.salesmap.retry', kwargs={'organizer': organizer.slug, 'event': event.slug, 'order': entry.order.pk})
207
+ })
208
+
209
+ avg_dist_val = round(sum(distances) / len(distances), 1) if distances else 0
210
+
211
+ return JsonResponse({
212
+ 'locations': locations_data, 'failed_orders': failed_orders_data,
213
+ 'event_marker': event_marker, 'milestones': milestones,
214
+ 'stats': {
215
+ 'top_cities': city_counter.most_common(10), 'top_items': item_counter.most_common(10),
216
+ 'total_orders': len(locations_data) + len(failed_orders_data),
217
+ 'geocoded_count': len(locations_data), 'avg_distance_km': avg_dist_val,
218
+ 'total_revenue': total_revenue, 'currency': event.currency
219
+ }
220
+ })
221
+ except Exception as e:
222
+ logger.exception(f"Error: {e}")
223
+ return JsonResponse({'error': _('Error')}, status=500)
224
+
225
+
226
+ class SalesMapView(EventSettingsViewMixin, TemplateView):
227
+ permission = 'can_view_orders'
228
+ template_name = 'pretix_mapplugin/map_page.html'
229
+
230
+ def get_context_data(self, **kwargs):
231
+ ctx = super().get_context_data(**kwargs)
232
+ ctx['other_events'] = self.request.organizer.events.exclude(pk=self.request.event.pk).order_by('-date_from')
233
+ return ctx
234
+
235
+ def get(self, request, *args, **kwargs):
236
+ try:
237
+ response = super().get(request, *args, **kwargs)
238
+ except Exception as e:
239
+ logger.exception(f"Error: {e}")
240
+ return HttpResponse(_("Error"), status=500)
241
+
242
+ current_csp = {}
243
+ header_key = 'Content-Security-Policy'
244
+ if header_key in response:
245
+ header_value = response[header_key]
246
+ if isinstance(header_value, bytes): header_value = header_value.decode('utf-8')
247
+ try:
248
+ from pretix.base.middleware import _parse_csp
249
+ current_csp = _parse_csp(header_value)
250
+ except Exception: current_csp = {}
251
+
252
+ # Allow external images for markers and Chart.js from CDN
253
+ map_csp_additions = {
254
+ 'img-src': [
255
+ 'https://*.tile.openstreetmap.org',
256
+ 'https://raw.githubusercontent.com',
257
+ 'https://cdnjs.cloudflare.com'
258
+ ],
259
+ 'script-src': [
260
+ 'https://cdn.jsdelivr.net',
261
+ "'unsafe-eval'" # Needed for some charting libs
262
+ ],
263
+ 'style-src': ["'unsafe-inline'"]
264
+ }
265
+
266
+ try:
267
+ from pretix.base.middleware import _merge_csp, _render_csp
268
+ _merge_csp(current_csp, map_csp_additions)
269
+ response[header_key] = _render_csp(current_csp)
270
+ except Exception: pass
271
+
272
+ return response
@@ -1,195 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: pretix-map
3
- Version: 0.1.3
4
- Summary: An overview map of the catchment area of previous orders. Measured by postcode
5
- Author-email: MarkenJaden <jjsch1410@gmail.com>
6
- Maintainer-email: MarkenJaden <jjsch1410@gmail.com>
7
- License: Apache
8
- Project-URL: homepage, https://github.com/MarkenJaden/pretix-map
9
- Project-URL: repository, https://github.com/MarkenJaden/pretix-map
10
- Keywords: pretix
11
- Description-Content-Type: text/x-rst
12
- License-File: LICENSE
13
- Requires-Dist: geopy
14
- Dynamic: license-file
15
-
16
- Map-Plugin
17
- ==========================
18
-
19
- This is a plugin for `pretix`_.
20
-
21
- It provides an overview map visualizing the geographic location of attendees based on the addresses provided in their orders. The plugin automatically geocodes order addresses upon payment and displays the locations on an interactive map within the event control panel.
22
-
23
- Features:
24
-
25
- * Automatic geocoding of paid order addresses using a configured geocoding service.
26
- * Interactive map display (Leaflet) showing locations as clustered pins or a heatmap.
27
- * Option to toggle between pin view and heatmap view.
28
- * Pins show tooltips with Order Code, Date, and Item Count on hover.
29
- * Clicking a pin navigates directly to the corresponding order details page.
30
- * Adds a "Sales Map" link to the event navigation sidebar.
31
- * Includes a management command to geocode orders placed *before* the plugin was installed or configured.
32
-
33
- Requirements
34
- ------------
35
-
36
- * A working `pretix installation`_.
37
- * A **Celery worker** configured and running for your Pretix instance. This is essential for the background geocoding tasks.
38
- * Access to a **Geocoding Service**. This plugin requires configuration to use an external service to convert addresses to latitude/longitude coordinates. See the **Configuration** section below.
39
-
40
-
41
- Installation (Production)
42
- --------------------------
43
-
44
- 1. Ensure you meet the requirements above, especially a running Celery worker.
45
- 2. Activate the virtual environment used for your Pretix installation.
46
- 3. Install the plugin via pip:
47
-
48
- .. code-block:: bash
49
-
50
- pip install pretix-map-plugin
51
-
52
- *(Note: If the plugin is not on PyPI yet, you might need to install from the git repository URL)*
53
- 4. Configure the required geocoding service settings in your `pretix.cfg` file (see **Configuration** below).
54
- 5. Restart your Pretix webserver (`gunicorn`/`uwsgi`) **and** your Pretix Celery worker(s).
55
- 6. Log in to your Pretix backend and go to Organizer Settings -> Plugins. Enable the "Sales Map" plugin for the desired organizer(s).
56
- 7. Go into an event, then navigate to Event Settings -> Plugins and ensure the "Sales Map" plugin is checked (enabled) for that event.
57
-
58
-
59
- Configuration (`pretix.cfg`)
60
- ------------------------------
61
-
62
- This plugin requires configuration in your `pretix.cfg` file to specify which geocoding service to use. Add a section `[pretix_mapplugin]` if it doesn't exist.
63
-
64
- **Required Setting (Choose ONE method):**
65
-
66
- * **Method 1: Nominatim (OpenStreetMap - Free, Requires User-Agent)**
67
- Nominatim is a free geocoding service based on OpenStreetMap data. It has usage policies that **require** you to set a descriptive User-Agent header, typically including your application name and contact email. Failure to do so may result in your IP being blocked.
68
-
69
- .. code-block:: ini
70
-
71
- [pretix_mapplugin]
72
- # REQUIRED for Nominatim: Set a descriptive User-Agent including application name and contact info.
73
- # Replace with your actual details! See: https://operations.osmfoundation.org/policies/nominatim/
74
- nominatim_user_agent=YourTicketingSite/1.0 (Contact: admin@yourdomain.com) pretix-map-plugin/1.0
75
-
76
- * **Method 2: Other Geocoding Services (e.g., Google, Mapbox - API Key likely needed)**
77
- *(This example assumes you have modified `tasks.py` to use a different geocoder like GeoPy with GoogleV3. Adjust the setting name and value based on your implementation.)*
78
-
79
- .. code-block:: ini
80
-
81
- [pretix_mapplugin]
82
- # Example for Google Geocoding API (if implemented in tasks.py)
83
- # google_geocoding_api_key=YOUR_GOOGLE_GEOCODING_API_KEY
84
-
85
- **Important:** After adding or changing settings in `pretix.cfg`, you **must restart** the Pretix webserver and Celery workers for the changes to take effect.
86
-
87
- Usage
88
- -----
89
-
90
- 1. Once installed, configured, and enabled, the plugin works mostly automatically.
91
- 2. When an order is marked as paid, a background task is queued to geocode the address associated with the order (typically the invoice address). This requires your Celery worker to be running.
92
- 3. A "Sales Map" link will appear in the event control panel's sidebar navigation (usually near other order-related items or plugin links) for users with the "Can view orders" permission.
93
- 4. Clicking this link displays the map. You can toggle between the pin view (markers clustered) and the heatmap view using the button provided.
94
- 5. In the pin view:
95
-
96
- * Hover over a marker cluster to see the number of orders it represents.
97
- * Zoom in to break clusters apart.
98
- * Hover over an individual pin to see a tooltip with Order Code, Date, and Item Count.
99
- * Click an individual pin to open the corresponding order details page in a new tab.
100
-
101
- Management Command: `geocode_existing_orders`
102
- ---------------------------------------------
103
-
104
- This command is essential for processing orders that were placed *before* the map plugin was installed, enabled, or correctly configured with geocoding credentials. It scans paid orders and queues geocoding tasks for those that haven't been geocoded yet.
105
-
106
- **When to Run:**
107
-
108
- * After installing and configuring the plugin for the first time.
109
- * If you previously ran the plugin without a working geocoding configuration or Celery worker.
110
- * If you want to force-reprocess orders (e.g., if geocoding logic changed).
111
-
112
- **Prerequisites:**
113
-
114
- * Your Pretix Celery worker **must** be running to process the tasks queued by this command.
115
- * Geocoding settings must be correctly configured in `pretix.cfg`.
116
-
117
- **How to Run:**
118
-
119
- 1. Navigate to your Pretix installation directory (containing `manage.py`) in your server terminal.
120
- 2. Activate your Pretix virtual environment.
121
- 3. Execute the command using `manage.py`.
122
-
123
- **Basic Command:**
124
-
125
- .. code-block:: bash
126
-
127
- python manage.py geocode_existing_orders [options]
128
-
129
- **Available Options:**
130
-
131
- * `--organizer <slug>`: Process orders only for the organizer with the given slug.
132
- * Example: `python manage.py geocode_existing_orders --organizer=myorg`
133
- * `--event <slug>`: Process orders only for the event with the given slug. **Requires** `--organizer` to be specified as well.
134
- * Example: `python manage.py geocode_existing_orders --organizer=myorg --event=myevent2024`
135
- * `--dry-run`: **Highly Recommended for first use!** Simulates the process and shows which orders *would* be queued, but doesn't actually queue any tasks. Use this to verify the scope and count before running for real.
136
- * Example: `python manage.py geocode_existing_orders --dry-run`
137
- * `--force-recode`: Queues geocoding tasks even for orders that already have an entry in the geocoding data table. Use this if you suspect previous geocoding attempts were incomplete or incorrect, or if the geocoding logic has been updated.
138
- * Example: `python manage.py geocode_existing_orders --organizer=myorg --force-recode`
139
-
140
- **Example Workflow:**
141
-
142
- 1. **Test with Dry Run (All Organizers):**
143
-
144
- .. code-block:: bash
145
-
146
- python manage.py geocode_existing_orders --dry-run
147
- 2. **(If satisfied) Run for Real (All Organizers):**
148
-
149
- .. code-block:: bash
150
-
151
- python manage.py geocode_existing_orders
152
- 3. **Monitor your Celery worker** logs to ensure tasks are being processed without errors.
153
-
154
-
155
- Development setup
156
- -----------------
157
-
158
- 1. Make sure that you have a working `pretix development setup`_. Ensure your dev setup includes a running Celery worker if you want to test the background tasks.
159
- 2. Clone this repository.
160
- 3. Activate the virtual environment you use for pretix development.
161
- 4. Execute ``python setup.py develop`` within this directory to register this application with pretix's plugin registry.
162
- 5. Execute ``make`` within this directory to compile translations.
163
- 6. **Configure Geocoding:** Add the necessary geocoding settings (e.g., `nominatim_user_agent`) to your local `pretix.cfg` file for testing the geocoding feature.
164
- 7. Restart your local pretix server and Celery worker. You can now use the plugin from this repository for your events by enabling it in the 'plugins' tab in the settings.
165
-
166
- This plugin has CI set up to enforce a few code style rules. To check locally, you need these packages installed::
167
-
168
- pip install flake8 isort black
169
-
170
- To check your plugin for rule violations, run::
171
-
172
- black --check .
173
- isort -c .
174
- flake8 .
175
-
176
- You can auto-fix some of these issues by running::
177
-
178
- isort .
179
- black .
180
-
181
- To automatically check for these issues before you commit, you can run ``.install-hooks``.
182
-
183
-
184
- License
185
- -------
186
-
187
- Copyright 2025 MarkenJaden
188
-
189
- Released under the terms of the Apache License 2.0
190
-
191
-
192
-
193
- .. _pretix: https://github.com/pretix/pretix
194
- .. _pretix installation: https://docs.pretix.eu/en/latest/administrator/installation/index.html
195
- .. _pretix development setup: https://docs.pretix.eu/en/latest/development/setup.html