pretix-map 0.1.4__py3-none-any.whl → 0.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. pretix_map-0.1.6.dist-info/METADATA +88 -0
  2. {pretix_map-0.1.4.dist-info → pretix_map-0.1.6.dist-info}/RECORD +32 -30
  3. {pretix_map-0.1.4.dist-info → pretix_map-0.1.6.dist-info}/WHEEL +1 -1
  4. {pretix_map-0.1.4.dist-info → pretix_map-0.1.6.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 +100 -51
  17. pretix_mapplugin/static/pretix_mapplugin/js/salesmap.js +611 -452
  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 +189 -88
  28. pretix_mapplugin/templates/pretix_mapplugin/milestones.html +53 -0
  29. pretix_mapplugin/urls.py +38 -21
  30. pretix_mapplugin/views.py +295 -163
  31. pretix_map-0.1.4.dist-info/METADATA +0 -195
  32. {pretix_map-0.1.4.dist-info → pretix_map-0.1.6.dist-info}/entry_points.txt +0 -0
  33. {pretix_map-0.1.4.dist-info → pretix_map-0.1.6.dist-info}/top_level.txt +0 -0
@@ -1,32 +1,32 @@
1
- # Generated by Django 4.2.20 on 2025-04-16 01:22
2
-
3
- from django.db import migrations, models
4
-
5
-
6
- class Migration(migrations.Migration):
7
-
8
- dependencies = [
9
- ('pretix_mapplugin', '0001_initial'),
10
- ]
11
-
12
- operations = [
13
- migrations.RemoveField(
14
- model_name='ordergeocodedata',
15
- name='geocoded_timestamp',
16
- ),
17
- migrations.AddField(
18
- model_name='ordergeocodedata',
19
- name='last_geocoded_at',
20
- field=models.DateTimeField(auto_now=True),
21
- ),
22
- migrations.AlterField(
23
- model_name='ordergeocodedata',
24
- name='latitude',
25
- field=models.FloatField(null=True),
26
- ),
27
- migrations.AlterField(
28
- model_name='ordergeocodedata',
29
- name='longitude',
30
- field=models.FloatField(null=True),
31
- ),
32
- ]
1
+ # Generated by Django 4.2.20 on 2025-04-16 01:22
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('pretix_mapplugin', '0001_initial'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RemoveField(
14
+ model_name='ordergeocodedata',
15
+ name='geocoded_timestamp',
16
+ ),
17
+ migrations.AddField(
18
+ model_name='ordergeocodedata',
19
+ name='last_geocoded_at',
20
+ field=models.DateTimeField(auto_now=True),
21
+ ),
22
+ migrations.AlterField(
23
+ model_name='ordergeocodedata',
24
+ name='latitude',
25
+ field=models.FloatField(null=True),
26
+ ),
27
+ migrations.AlterField(
28
+ model_name='ordergeocodedata',
29
+ name='longitude',
30
+ field=models.FloatField(null=True),
31
+ ),
32
+ ]
@@ -0,0 +1,27 @@
1
+ from django.db import migrations, models
2
+ import django.db.models.deletion
3
+
4
+
5
+ class Migration(migrations.Migration):
6
+
7
+ dependencies = [
8
+ ('pretixbase', '0001_initial'),
9
+ ('pretix_mapplugin', '0002_remove_ordergeocodedata_geocoded_timestamp_and_more'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.CreateModel(
14
+ name='MapMilestone',
15
+ fields=[
16
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
17
+ ('date', models.DateField(verbose_name='Date')),
18
+ ('label', models.CharField(max_length=200, verbose_name='Milestone Label')),
19
+ ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='map_milestones', to='pretixbase.Event')),
20
+ ],
21
+ options={
22
+ 'verbose_name': 'Map Milestone',
23
+ 'verbose_name_plural': 'Map Milestones',
24
+ 'ordering': ['date'],
25
+ },
26
+ ),
27
+ ]
@@ -1,47 +1,71 @@
1
- from django.db import models
2
- from pretix.base.models import LoggedModel, Order
3
-
4
-
5
- class OrderGeocodeData(LoggedModel): # Keep LoggedModel if you want audit logs
6
- """
7
- Stores the geocoded coordinates for a Pretix Order's invoice address.
8
- Allows null coordinates for failed geocoding attempts.
9
- """
10
- order = models.OneToOneField(
11
- Order,
12
- on_delete=models.CASCADE,
13
- related_name='geocode_data',
14
- primary_key=True # Keep this if you want order PK as primary key
15
- )
16
- latitude = models.FloatField(
17
- null=True, # Allow NULL in the database if geocoding fails
18
- blank=True # Allow blank in forms/admin (good practice with null=True)
19
- )
20
- longitude = models.FloatField(
21
- null=True, # Allow NULL in the database if geocoding fails
22
- blank=True # Allow blank in forms/admin
23
- )
24
-
25
- # Change to auto_now to update timestamp on every save (successful or null)
26
- last_geocoded_at = models.DateTimeField(
27
- auto_now=True, # Set/Update timestamp every time the record is saved
28
- help_text="Timestamp when geocoding was last attempted/updated."
29
- )
30
-
31
- class Meta:
32
- verbose_name = "Order Geocode Data"
33
- verbose_name_plural = "Order Geocode Data"
34
- # Optional: Indexing coordinates can speed up map data queries if you have many entries
35
- # indexes = [
36
- # models.Index(fields=['latitude', 'longitude']),
37
- # ]
38
-
39
- def __str__(self):
40
- # Provide more informative string representation
41
- if self.latitude is not None and self.longitude is not None:
42
- return f"Geocode for Order {self.order.code}: ({self.latitude:.4f}, {self.longitude:.4f})"
43
- else:
44
- # Indicate if it's pending (never attempted) or failed (null coords stored)
45
- # This requires knowing if the record exists but has nulls vs doesn't exist yet
46
- # The current __str__ assumes the record exists if called.
47
- return f"Geocode data for Order {self.order.code} (Coordinates: None)"
1
+ from django.db import models
2
+ from pretix.base.models import LoggedModel, Order
3
+
4
+
5
+ class OrderGeocodeData(LoggedModel): # Keep LoggedModel if you want audit logs
6
+ """
7
+ Stores the geocoded coordinates for a Pretix Order's invoice address.
8
+ Allows null coordinates for failed geocoding attempts.
9
+ """
10
+ order = models.OneToOneField(
11
+ Order,
12
+ on_delete=models.CASCADE,
13
+ related_name='geocode_data',
14
+ primary_key=True # Keep this if you want order PK as primary key
15
+ )
16
+ latitude = models.FloatField(
17
+ null=True, # Allow NULL in the database if geocoding fails
18
+ blank=True # Allow blank in forms/admin (good practice with null=True)
19
+ )
20
+ longitude = models.FloatField(
21
+ null=True, # Allow NULL in the database if geocoding fails
22
+ blank=True # Allow blank in forms/admin
23
+ )
24
+
25
+ # Change to auto_now to update timestamp on every save (successful or null)
26
+ last_geocoded_at = models.DateTimeField(
27
+ auto_now=True, # Set/Update timestamp every time the record is saved
28
+ help_text="Timestamp when geocoding was last attempted/updated."
29
+ )
30
+
31
+ class Meta:
32
+ verbose_name = "Order Geocode Data"
33
+ verbose_name_plural = "Order Geocode Data"
34
+ # Optional: Indexing coordinates can speed up map data queries if you have many entries
35
+ # indexes = [
36
+ # models.Index(fields=['latitude', 'longitude']),
37
+ # ]
38
+
39
+ def __str__(self):
40
+ # Provide more informative string representation
41
+ if self.latitude is not None and self.longitude is not None:
42
+ return f"Geocode for Order {self.order.code}: ({self.latitude:.4f}, {self.longitude:.4f})"
43
+ else:
44
+ return f"Geocode data for Order {self.order.code} (Coordinates: None)"
45
+
46
+
47
+ class MapMilestone(models.Model):
48
+ """
49
+ Stores marketing milestones (e.g., newsletters, ad starts) for an event.
50
+ Used for visualization in the Sales Map timeline.
51
+ """
52
+ event = models.ForeignKey(
53
+ 'pretixbase.Event',
54
+ on_delete=models.CASCADE,
55
+ related_name='map_milestones'
56
+ )
57
+ date = models.DateField(
58
+ verbose_name="Date"
59
+ )
60
+ label = models.CharField(
61
+ max_length=200,
62
+ verbose_name="Milestone Label"
63
+ )
64
+
65
+ class Meta:
66
+ ordering = ['date']
67
+ verbose_name = "Map Milestone"
68
+ verbose_name_plural = "Map Milestones"
69
+
70
+ def __str__(self):
71
+ return f"{self.date}: {self.label} ({self.event.slug})"
@@ -1,92 +1,77 @@
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
- from django.conf import settings
7
-
8
- # --- Pretix Signals ---
9
- from pretix.base.signals import order_paid
10
- from pretix.control.signals import nav_event
11
-
12
- # --- Tasks ---
13
- from .tasks import geocode_order_task
14
- # --- Geocoding Default ---
15
- from .geocoding import DEFAULT_NOMINATIM_USER_AGENT
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
- # --- Constants ---
20
- MAP_VIEW_URL_NAME = 'plugins:pretix_mapplugin:event.settings.salesmap.show'
21
- REQUIRED_MAP_PERMISSION = 'can_view_orders'
22
- PLUGIN_NAME = 'pretix_mapplugin'
23
-
24
-
25
- # --- Signal Receiver for Geocoding (Passes organizer_pk) ---
26
- @receiver(order_paid, dispatch_uid="sales_mapper_order_paid_geocode")
27
- def trigger_geocoding_on_payment(sender, order, **kwargs):
28
- """
29
- Listens for the order_paid signal, reads geocoding config,
30
- and queues the geocoding task with order_pk, organizer_pk, and config.
31
- """
32
- user_agent = DEFAULT_NOMINATIM_USER_AGENT
33
- organizer_pk = None # Initialize
34
- try:
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 ---
43
- if hasattr(settings, 'plugins') and hasattr(settings.plugins, PLUGIN_NAME):
44
- plugin_settings = getattr(settings.plugins, PLUGIN_NAME)
45
- user_agent = plugin_settings.get('nominatim_user_agent', DEFAULT_NOMINATIM_USER_AGENT)
46
- else:
47
- logger.warning(f"Could not access settings.plugins.{PLUGIN_NAME}, using default User-Agent.")
48
-
49
- # --- Queue task with user_agent and organizer_pk as keyword arguments ---
50
- geocode_order_task.apply_async(
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
- }
56
- )
57
- logger.info(f"Geocoding task queued for paid order {order.code} (PK: {order.pk}, Org PK: {organizer_pk}).")
58
-
59
- except ImportError:
60
- logger.exception("Could not import geocode_order_task. Check tasks.py.")
61
- except Exception as 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}")
65
-
66
-
67
- # --- Signal Receiver for Adding Navigation Item (No changes needed) ---
68
- @receiver(nav_event, dispatch_uid="sales_mapper_nav_event_add_map")
69
- def add_map_nav_item(sender, request: HttpRequest, **kwargs):
70
- """
71
- Adds a navigation item for the Sales Map to the event control panel sidebar.
72
- """
73
- has_permission = request.user.has_event_permission(request.organizer, request.event, REQUIRED_MAP_PERMISSION,
74
- request=request)
75
- if not has_permission: return []
76
- try:
77
- map_url = reverse(MAP_VIEW_URL_NAME, kwargs={
78
- 'organizer': request.organizer.slug,
79
- 'event': request.event.slug,
80
- })
81
- except NoReverseMatch:
82
- logger.error(f"Could not reverse URL for map view '{MAP_VIEW_URL_NAME}'. Check urls.py.")
83
- return []
84
- is_active = False
85
- if hasattr(request, 'resolver_match') and request.resolver_match:
86
- is_active = request.resolver_match.view_name == MAP_VIEW_URL_NAME
87
- return [{
88
- 'label': _('Sales Map'),
89
- 'url': map_url,
90
- 'active': is_active,
91
- 'icon': 'map-o',
92
- }]
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
+ from django.conf import settings
7
+ from django.utils.timezone import now
8
+ from django.db import transaction
9
+
10
+ # --- Pretix Signals ---
11
+ from pretix.base.signals import order_placed, periodic_task
12
+ from pretix.control.signals import nav_event
13
+
14
+ # --- Tasks ---
15
+ from .tasks import geocode_order_task, nightly_reprocess_geocoding
16
+ # --- Geocoding Default ---
17
+ from .geocoding import DEFAULT_NOMINATIM_USER_AGENT
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # --- Constants ---
22
+ MAP_VIEW_URL_NAME = 'plugins:pretix_mapplugin:event.settings.salesmap.show'
23
+ REQUIRED_MAP_PERMISSION = 'can_view_orders'
24
+ PLUGIN_NAME = 'pretix_mapplugin'
25
+
26
+
27
+ @receiver(periodic_task, dispatch_uid="sales_mapper_periodic_reprocess")
28
+ def trigger_nightly_geocoding(sender, **kwargs):
29
+ if now().hour == 3 and now().minute < 10:
30
+ nightly_reprocess_geocoding.apply_async()
31
+
32
+
33
+ @receiver(order_placed, dispatch_uid="sales_mapper_order_placed_geocode")
34
+ def trigger_geocoding_on_placement(sender, order, **kwargs):
35
+ """
36
+ Queues geocoding after the current transaction is committed to avoid SQLite locks.
37
+ """
38
+ user_agent = DEFAULT_NOMINATIM_USER_AGENT
39
+ if hasattr(settings, 'CONFIG_FILE') and settings.CONFIG_FILE.has_section('pretix_mapplugin'):
40
+ user_agent = settings.CONFIG_FILE.get('pretix_mapplugin', 'nominatim_user_agent', fallback=DEFAULT_NOMINATIM_USER_AGENT)
41
+
42
+ def run_task():
43
+ geocode_order_task.apply_async(
44
+ args=[order.pk],
45
+ kwargs={
46
+ 'nominatim_user_agent': user_agent,
47
+ 'organizer_pk': order.event.organizer.pk
48
+ }
49
+ )
50
+
51
+ # Crucial: Only run AFTER the order has been saved to DB
52
+ transaction.on_commit(run_task)
53
+
54
+
55
+ @receiver(nav_event, dispatch_uid="sales_mapper_nav_event_add_map")
56
+ def add_map_nav_item(sender, request: HttpRequest, **kwargs):
57
+ has_permission = request.user.has_event_permission(request.organizer, request.event, REQUIRED_MAP_PERMISSION, request=request)
58
+ if not has_permission: return []
59
+ try:
60
+ map_url = reverse(MAP_VIEW_URL_NAME, kwargs={'organizer': request.organizer.slug, 'event': request.event.slug})
61
+ milestone_url = reverse('plugins:pretix_mapplugin:event.settings.salesmap.milestones', kwargs={'organizer': request.organizer.slug, 'event': request.event.slug})
62
+ except NoReverseMatch:
63
+ return []
64
+
65
+ is_active = request.resolver_match.view_name == MAP_VIEW_URL_NAME if request.resolver_match else False
66
+ is_milestone_active = request.resolver_match.view_name == 'plugins:pretix_mapplugin:event.settings.salesmap.milestones' if request.resolver_match else False
67
+
68
+ return [{
69
+ 'label': _('Sales Map'),
70
+ 'url': map_url,
71
+ 'active': is_active or is_milestone_active,
72
+ 'icon': 'map-o',
73
+ 'children': [
74
+ {'label': _('Map View'), 'url': map_url, 'active': is_active},
75
+ {'label': _('Milestones'), 'url': milestone_url, 'active': is_milestone_active},
76
+ ]
77
+ }]
@@ -1,52 +1,101 @@
1
- .plugin-map-content-wrapper {
2
- display: flex;
3
- flex-direction: column;
4
- height: calc(100vh - 100px);
5
- min-height: 450px;
6
- }
7
-
8
- .plugin-map-content-wrapper h1,
9
- .plugin-map-content-wrapper .form-group {
10
- flex-shrink: 0;
11
- margin-bottom: 1em;
12
- }
13
-
14
- .map-wrapper {
15
- flex-grow: 1;
16
- position: relative;
17
- min-height: 0;
18
- border: 1px solid #ccc;
19
- }
20
-
21
- #sales-map-container {
22
- height: 100%;
23
- width: 100%;
24
- display: block;
25
- position: relative;
26
- }
27
-
28
- #map-status-overlay {
29
- position: absolute;
30
- top: 0;
31
- left: 0;
32
- width: 100%;
33
- height: 100%;
34
- }
35
-
36
- #map-status-overlay p {
37
- padding: 1em;
38
- background: #fff;
39
- border-radius: 5px;
40
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
41
- }
42
-
43
- #heatmap-options-panel input[type=range] {
44
- width: 100%;
45
- display: block;
46
- }
47
-
48
- #map-status-overlay p.text-danger {
49
- color: #a94442;
50
- background-color: #f2dede;
51
- border-color: #ebccd1;
1
+ .plugin-map-content-wrapper {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: calc(100vh - 100px);
5
+ min-height: 450px;
6
+ }
7
+
8
+ .plugin-map-content-wrapper h1,
9
+ .plugin-map-content-wrapper .form-group {
10
+ flex-shrink: 0;
11
+ margin-bottom: 1em;
12
+ }
13
+
14
+ .map-wrapper {
15
+ flex-grow: 1;
16
+ position: relative;
17
+ min-height: 0;
18
+ border: 1px solid #ccc;
19
+ display: flex;
20
+ flex-direction: column;
21
+ }
22
+
23
+ #sales-map-container {
24
+ height: 100%;
25
+ width: 100%;
26
+ display: block;
27
+ position: relative;
28
+ }
29
+
30
+ /* Side-by-side comparison styles */
31
+ .map-split-container {
32
+ display: flex;
33
+ flex-direction: row;
34
+ width: 100%;
35
+ flex-grow: 1;
36
+ min-height: 500px;
37
+ }
38
+
39
+ #sales-map-compare-container {
40
+ width: 50%;
41
+ height: 100%;
42
+ border-left: 2px solid #fff;
43
+ display: none;
44
+ }
45
+
46
+ .map-split-active #sales-map-container {
47
+ width: 50% !important;
48
+ }
49
+
50
+ .map-split-active #sales-map-compare-container {
51
+ display: block;
52
+ }
53
+
54
+ .map-label-overlay {
55
+ position: absolute;
56
+ top: 10px;
57
+ left: 50px;
58
+ z-index: 1000;
59
+ background: rgba(255, 255, 255, 0.8);
60
+ padding: 2px 8px;
61
+ border-radius: 4px;
62
+ pointer-events: none;
63
+ font-weight: bold;
64
+ border: 1px solid #ccc;
65
+ }
66
+
67
+ #timeline-chart-container {
68
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
69
+ }
70
+
71
+ .timeline-milestone-marker:hover span {
72
+ background: rgba(255, 255, 255, 1) !important;
73
+ z-index: 10;
74
+ border: 1px solid #ccc;
75
+ }
76
+
77
+ #map-status-overlay {
78
+ position: absolute;
79
+ top: 0;
80
+ left: 0;
81
+ width: 100%;
82
+ height: 100%;
83
+ }
84
+
85
+ #map-status-overlay p {
86
+ padding: 1em;
87
+ background: #fff;
88
+ border-radius: 5px;
89
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
90
+ }
91
+
92
+ #heatmap-options-panel input[type=range] {
93
+ width: 100%;
94
+ display: block;
95
+ }
96
+
97
+ #map-status-overlay p.text-danger {
98
+ color: #a94442;
99
+ background-color: #f2dede;
100
+ border-color: #ebccd1;
52
101
  }