Repository: pretix/pretix

Issue:
Configurable subevent ordering
At the moment, subevent ordering mainly depends on their start date. This is not always the most intuitive, e.g. when there are events in two different locations at various times and people mainly decide which event to attend based on the events location. If the subevents where grouped by in this case location, it would be easier to find the desired subevent in the list on the presale page.

We could

- make the subevent positions configurable in a per subevent basis (like in many other places in pretix, where paginated item list items can be reordered), or
- provide functionality to chose an ordering method (by event start, event name, location->name, ...)

What do you think?




Discussion:
> make the subevent positions configurable in a per subevent basis

I'm not sure this is a good idea, it seems like it will create unexpected behaviour to most users.

> provide functionality to chose an ordering method (by event start, event name, location->name, ...)

Yes, that sounsd better.
Ok, lets create a setting that influences the sorting behaviour in subevent lists on the presale page.
Sorting for this happens in this line of a property function called from a template:
https://github.com/pretix/pretix/blob/1ee584c5a1e7b041d94add6b410f5025d7e379bd/src/pretix/base/models/event.py#L536

My plan is to create a setting for this under Eventsettings->Display. I'm not sure where to put the logic for which sorting tuple to use though. (The setting value might be `'name__asc,date_from__asc'`. How and where is this 'translated' to the tuple we can pass as argument to `order_by`.)

Resolving patch:
diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py
--- a/src/pretix/base/models/event.py
+++ b/src/pretix/base/models/event.py
@@ -2,6 +2,7 @@
 import uuid
 from collections import OrderedDict
 from datetime import datetime, time
+from operator import attrgetter
 
 import pytz
 from django.conf import settings
@@ -535,6 +536,23 @@ def active_future_subevents(self):
             )
         ).order_by('date_from', 'name')
 
+    @property
+    def subevent_list_subevents(self):
+        ordering = self.settings.get('frontpage_subevent_ordering', default='date_ascending', as_type=str)
+        orderfields = {
+            'date_ascending': ('date_from', 'name'),
+            'date_descending': ('-date_from', 'name'),
+            'name_ascending': ('name', 'date_from'),
+            'name_descending': ('-name', 'date_from'),
+        }[ordering]
+        subevs = self.subevents.filter(
+            Q(active=True) & (
+                Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
+                | Q(date_to__gte=now())
+            )
+        )  # order_by doesn't make sense with I18nField
+        return sorted(subevs, key=attrgetter(*orderfields))
+
     @property
     def meta_data(self):
         data = {p.name: p.default for p in self.organizer.meta_properties.all()}
diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py
--- a/src/pretix/base/settings.py
+++ b/src/pretix/base/settings.py
@@ -495,6 +495,10 @@
         'default': '',
         'type': LazyI18nString
     },
+    'frontpage_subevent_ordering': {
+        'default': 'date_ascending',
+        'type': str
+    },
 }
 
 settings_hierarkey = Hierarkey(attribute_name='settings')
diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py
--- a/src/pretix/control/forms/event.py
+++ b/src/pretix/control/forms/event.py
@@ -6,7 +6,9 @@
 from django.db.models import Q
 from django.forms import formset_factory
 from django.utils.timezone import get_current_timezone_name
-from django.utils.translation import pgettext_lazy, ugettext_lazy as _
+from django.utils.translation import (
+    pgettext, pgettext_lazy, ugettext_lazy as _,
+)
 from django_countries import Countries
 from django_countries.fields import LazyTypedChoiceField
 from i18nfield.forms import (
@@ -854,12 +856,24 @@ class DisplaySettingsForm(SettingsForm):
         label=_("Show variations of a product expanded by default"),
         required=False
     )
+    frontpage_subevent_ordering = forms.ChoiceField(
+        label=pgettext('subevent', 'Date ordering'),
+        choices=[
+            ('date_ascending', _('Event start time')),
+            ('date_descending', _('Event start time (descending)')),
+            ('name_ascending', _('Name')),
+            ('name_descending', _('Name (descending)')),
+        ],  # When adding a new ordering, remember to also define it in the event model
+    )
 
     def __init__(self, *args, **kwargs):
+        event = kwargs['obj']
         super().__init__(*args, **kwargs)
         self.fields['primary_font'].choices += [
             (a, a) for a in get_fonts()
         ]
+        if not event.has_subevents:
+            del self.fields['frontpage_subevent_ordering']
 
 
 class TicketSettingsForm(SettingsForm):


Test patch:
diff --git a/src/tests/presale/test_event.py b/src/tests/presale/test_event.py
--- a/src/tests/presale/test_event.py
+++ b/src/tests/presale/test_event.py
@@ -153,7 +153,7 @@ def test_subevents_inactive_unknown(self):
         resp = self.client.get('/%s/%s/%d/' % (self.orga.slug, self.event.slug, se1.pk + 1000))
         assert resp.status_code == 404
 
-    def test_subevent_list(self):
+    def test_subevent_list_activeness(self):
         self.event.has_subevents = True
         self.event.save()
         self.event.subevents.create(name='Foo SE1', date_from=now() + datetime.timedelta(days=1), active=True)
@@ -162,6 +162,20 @@ def test_subevent_list(self):
         self.assertIn("Foo SE1", resp.rendered_content)
         self.assertNotIn("Foo SE2", resp.rendered_content)
 
+    def test_subevent_list_ordering(self):
+        self.event.has_subevents = True
+        self.event.save()
+        self.event.subevents.create(name='Epic SE', date_from=now() + datetime.timedelta(days=1), active=True)
+        self.event.subevents.create(name='Cool SE', date_from=now() + datetime.timedelta(days=2), active=True)
+
+        self.event.settings.frontpage_subevent_ordering = 'date_ascending'
+        content = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug)).rendered_content
+        self.assertLess(content.index('Epic SE'), content.index('Cool SE'))
+
+        self.event.settings.frontpage_subevent_ordering = 'name_ascending'
+        content = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug)).rendered_content
+        self.assertLess(content.index('Cool SE'), content.index('Epic SE'))
+
     def test_subevent_calendar(self):
         self.event.settings.event_list_type = 'calendar'
         self.event.has_subevents = True