Repository: dotkom/onlineweb4

Issue:
Sort events on front page by registration start rather than event start
This way we won't have problems with events having registration before even being on the front page and the listing will actually be based on when one has to actually do something with regards to an event. 

I can't really remember whether this was ever brought up as an option when designing the site ( @lizter, @melwil ). Personally I like the idea, but I'd like some input. 

Featured events (ref Åre being pushed outside frontpage).
By mail request

Like the "featured" flag on articles



Discussion:
Would events like Åre, ITEX, Hovedekskursjon stay at the first position until they are done?

No, they could fall back to be listed based on event start after registration has ended or after a given amount of time. 

Sounds good!

I have a counter proposal to this, which is to feature events in the order that registration becomes available. If it doesn't have a registration start, just use event start.

You can then display it on the front in one of two ways, I imagine:
- Sorted after registration_start until event_end has passed
- Sorted after registration_start until registration_end, then sort on event_start

This makes it unnecessary to implement this flag, and a simple property in events can make it easy for each event to tell a system what is wishes to be sorted by.

Only "issue" here is that you can't use a plain `.order_by` to sort them of this strategy is used, unless you can somehow get the ordering to take registration into account.

> - Sorted after registration_start until event_end has passed

Would make events like Åre and Hovedekskursjon stay in the first position(s) until they pass, which is not the wanted result.

> - Sorted after registration_start until registration_end, then sort on event_start

Is a nice implementation.

> Only "issue" here is that you can't use a plain `.order_by` to sort them of this strategy is used, unless you can somehow get the ordering to take registration into account.

I'm sure you can, e.g. using a manager. (Which renders your statement correct: not using a simple order_by.)

Well, thoughts on the second one? It's the one I consider the most sane, and it basically solves the problem that the "featured" flag tries to solve, without any user intervention needed.

I'll have a look at it and push a PR.


Resolving patch:
diff --git a/apps/events/models.py b/apps/events/models.py
--- a/apps/events/models.py
+++ b/apps/events/models.py
@@ -8,6 +8,7 @@
 from django.contrib.auth.models import Group
 from django.contrib.contenttypes.models import ContentType
 from django.db import models
+from django.db.models import Case, Q, When
 from django.template.defaultfilters import slugify
 from django.utils import timezone
 from django.utils.translation import ugettext as _
@@ -31,6 +32,29 @@
 )
 
 
+# Managers
+
+class EventOrderedByRegistration(models.Manager):
+    """
+    Order events by registration start if registration start is within 7 days of today.
+    """
+    def get_queryset(self):
+        DELTA_FUTURE = settings.OW4_SETTINGS.get('events').get('OW4_EVENTS_FEATURED_DAYS_FUTURE')
+        DELTA_PAST = settings.OW4_SETTINGS.get('events').get('OW4_EVENTS_FEATURED_DAYS_PAST')
+        week_back = timezone.now() - timedelta(days=DELTA_PAST)
+        week_in_future = timezone.now() + timedelta(days=DELTA_FUTURE)
+
+        return super(EventOrderedByRegistration, self).get_queryset().\
+            annotate(registration_filtered=Case(
+                When(Q(attendance_event__registration_start__gte=week_back) &
+                     Q(attendance_event__registration_start__lte=week_in_future),
+                     then='attendance_event__registration_start'),
+                default='event_end',
+                output_field=models.DateTimeField()
+            )
+        ).order_by('registration_filtered')
+
+
 class Event(models.Model):
     """
     Base class for Event-objects.
@@ -39,6 +63,10 @@ class Event(models.Model):
     IMAGE_FOLDER = "images/events"
     IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.gif', '.png', '.tif', '.tiff']
 
+    # Managers
+    objects = models.Manager()
+    by_registration = EventOrderedByRegistration()
+
     author = models.ForeignKey(User, related_name='oppretter')
     title = models.CharField(_('tittel'), max_length=60)
     event_start = models.DateTimeField(_('start-dato'))
diff --git a/apps/events/views.py b/apps/events/views.py
--- a/apps/events/views.py
+++ b/apps/events/views.py
@@ -300,11 +300,15 @@ class EventViewSet(viewsets.GenericViewSet, mixins.RetrieveModelMixin, mixins.Li
     permission_classes = (AllowAny,)
     filter_class = EventDateFilter
     filter_fields = ('event_start', 'event_end', 'id',)
-    ordering_fields = ('event_start', 'event_end', 'id',)
-    ordering = ('id',)
+    ordering_fields = ('event_start', 'event_end', 'id', 'registration_filtered',)
+    ordering = ('id', 'registration_filtered',)
 
     def get_queryset(self):
-        return Event.objects.filter(
+        """
+        :return: Queryset filtered by these requirements:
+        - event has NO group restriction OR user having access to restricted event
+        """
+        return Event.by_registration.filter(
             Q(group_restriction__isnull=True) | Q(group_restriction__groups__in=self.request.user.groups.all())).\
             distinct()
 
diff --git a/onlineweb4/settings/base.py b/onlineweb4/settings/base.py
--- a/onlineweb4/settings/base.py
+++ b/onlineweb4/settings/base.py
@@ -398,6 +398,13 @@
 CORS_ORIGIN_ALLOW_ALL = True
 CORS_URLS_REGEX = r'^/api/v1/.*$' # Enables CORS on /api/v1/ endpoints only
 
+OW4_SETTINGS = {
+   'events': {
+       'FEATURED_DAYS_FUTURE': os.getenv('OW4_EVENTS_FEATURED_DAYS_FUTURE', 7),
+       'FEATURED_DAYS_PAST': os.getenv('OW4_EVENTS_FEATURED_DAYS_PAST', 7),
+   }
+}
+
 # Remember to keep 'local' last, so it can override any setting.
 for settings_module in ['filebrowser', 'django_wiki', 'local']:  # local last
     if not os.path.exists(os.path.join(PROJECT_SETTINGS_DIRECTORY,


Test patch:
diff --git a/apps/events/tests.py b/apps/events/tests.py
--- a/apps/events/tests.py
+++ b/apps/events/tests.py
@@ -3,9 +3,10 @@
 import datetime
 import logging
 
+from django.conf import settings
 from django.contrib.auth.models import Group
 from django.core.urlresolvers import reverse
-from django.test import TestCase
+from django.test import TestCase, override_settings
 from django.utils import timezone
 from django_dynamic_fixture import G
 from rest_framework import status
@@ -32,7 +33,7 @@ class EventTest(TestCase):
 
     def setUp(self):
         self.event = G(Event, title='Sjakkturnering')
-        self.attendance_event = G(AttendanceEvent, event=self.event)
+        self.attendance_event = G(AttendanceEvent, event=self.event, max_capacity=2)
         self.user = G(User, username='ola123', ntnu_username='ola123ntnu', first_name="ola", last_name="nordmann")
         self.attendee = G(Attendee, event=self.attendance_event, user=self.user)
         self.logger = logging.getLogger(__name__)
@@ -383,6 +384,68 @@ def testRestrictedEvents(self):
                         "Any user should be able to see unrestricted events")
 
 
+class EventOrderedByRegistrationTestCase(TestCase):
+    def setUp(self):
+        self.FEATURED_TIMEDELTA_SETTINGS = settings
+        # Override settings so that the tests will work even if we update the default delta
+        self.FEATURED_TIMEDELTA_SETTINGS.OW4_SETTINGS['events']['OW4_EVENTS_FEATURED_DAYS_FUTURE'] = 7
+        self.FEATURED_TIMEDELTA_SETTINGS.OW4_SETTINGS['events']['OW4_EVENTS_FEATURED_DAYS_PAST'] = 7
+
+    def test_registration_no_push_forward(self):
+        """
+        Tests that an AttendanceEvent with registration date far in the future is sorted by its event end date,
+        like any other event.
+        """
+        today = timezone.now()
+        month_ahead = today + datetime.timedelta(days=30)
+        month_ahead_plus_five = month_ahead + datetime.timedelta(days=5)
+        normal_event = G(Event, event_start=month_ahead, event_end=month_ahead)
+        pushed_event = G(Event, event_start=month_ahead_plus_five, event_end=month_ahead_plus_five)
+        G(AttendanceEvent, registration_start=month_ahead_plus_five, registration_end=month_ahead_plus_five,
+          event=pushed_event)
+
+        expected_order = [normal_event, pushed_event]
+
+        with override_settings(settings=self.FEATURED_TIMEDELTA_SETTINGS):
+            self.assertEqual(list(Event.by_registration.all()), expected_order)
+
+    def test_registration_start_pushed_forward(self):
+        """
+        Tests that an AttendanceEvent with registration date within the "featured delta" (+/- 7 days from today)
+        will be pushed ahead in the event list, thus sorted by registration start rather than event end.
+        """
+        today = timezone.now()
+        three_days_ahead = today + datetime.timedelta(days=3)
+        month_ahead = today + datetime.timedelta(days=30)
+        month_ahead_plus_five = month_ahead + datetime.timedelta(days=5)
+        normal_event = G(Event, event_start=month_ahead, event_end=month_ahead)
+        pushed_event = G(Event, event_start=month_ahead_plus_five, event_end=month_ahead_plus_five)
+        G(AttendanceEvent, registration_start=three_days_ahead, registration_end=three_days_ahead, event=pushed_event)
+
+        expected_order = [pushed_event, normal_event]
+
+        with override_settings(settings=self.FEATURED_TIMEDELTA_SETTINGS):
+            self.assertEqual(list(Event.by_registration.all()), expected_order)
+
+    def test_registration_past_push_forward(self):
+        """
+        Tests that an AttendanceEvent with a registration date in the past, outside the "featured delta" (+/- 7 days)
+        will be sorted by the event's end date.
+        """
+        today = timezone.now()
+        month_ahead = today + datetime.timedelta(days=30)
+        month_ahead_plus_five = month_ahead + datetime.timedelta(days=5)
+        month_back = today - datetime.timedelta(days=30)
+        normal_event = G(Event, event_start=month_ahead, event_end=month_ahead)
+        pushed_event = G(Event, event_start=month_ahead_plus_five, event_end=month_ahead_plus_five)
+        G(AttendanceEvent, registration_start=month_back, registration_end=month_back, event=pushed_event)
+
+        expected_order = [normal_event, pushed_event]
+
+        with override_settings(settings=self.FEATURED_TIMEDELTA_SETTINGS):
+            self.assertEqual(list(Event.by_registration.all()), expected_order)
+
+
 class EventsURLTestCase(TestCase):
     def test_events_index_empty(self):
         url = reverse('events_index')