From 7bfda78e91c85097d4de1dd6720bb72f101ae892 Mon Sep 17 00:00:00 2001
From: ingo <ingo.leindecker@fro.at>
Date: Wed, 14 Mar 2018 18:55:38 +0100
Subject: [PATCH] Extended rrule wrapper

* for adding a number of (business) days to the schedule (e.g. "on the rrules' following day")
* fixed a bug for monthly recurrences
* and some minor changes

See #8
---
 README.rst                                    |  7 ++-
 program/admin.py                              | 24 ++++----
 program/migrations/0017_auto_20180314_1409.py | 25 +++++++++
 program/models.py                             | 56 ++++++++++++++++++-
 program/serializers.py                        |  2 +
 program/templates/boxes/broadcastformat.html  | 11 ----
 program/templates/collisions.html             |  4 +-
 program/templatetags/content_boxes.py         |  6 +-
 program/views.py                              | 10 ++--
 pv/settings.py                                |  3 +
 10 files changed, 116 insertions(+), 32 deletions(-)
 create mode 100644 program/migrations/0017_auto_20180314_1409.py
 delete mode 100644 program/templates/boxes/broadcastformat.html

diff --git a/README.rst b/README.rst
index 0302391c..b03edc78 100644
--- a/README.rst
+++ b/README.rst
@@ -42,10 +42,9 @@ Setting up the database
 
 By default the project is set up to run on a SQLite database.
 
-Create a file pv/local_settings.py and add at least the two lines::
+Create a file pv/local_settings.py and add at least the following line::
 
     SECRET_KEY = 'secret key'
-    USE_TZ = False
 
 (obviously replacing "secret key" with a key of your choice).
 
@@ -54,6 +53,10 @@ Then run::
     (python)$ python manage.py migrate
     (python)$ python manage.py loaddata program/fixtures/*.yaml
 
+Open pv/local_settings.py again and add the line::
+
+    USE_TZ = False
+
 Setting up MySQL
 ----------------
 
diff --git a/program/admin.py b/program/admin.py
index b72afd8b..3eeb13fd 100644
--- a/program/admin.py
+++ b/program/admin.py
@@ -404,6 +404,8 @@ class ShowAdmin(admin.ModelAdmin):
                 is_repetition = request.POST.get('ps_save_is_repetition')
                 automation_id = int(request.POST.get('ps_save_automation_id')) if request.POST.get('ps_save_automation_id') != 'None' else 0
                 fallback_id = int(request.POST.get('ps_save_fallback_id')) if request.POST.get('ps_save_fallback_id') != 'None' else 0
+                add_days_no = int(request.POST.get('ps_save_add_days_no')) if request.POST.get('ps_save_add_days_no') != 'None' and int(request.POST.get('ps_save_add_days_no')) > 0 else None
+                add_business_days_only = request.POST.get('ps_save_add_business_days_only')
 
                 # Put timeslot POST vars into lists with same indices
                 for i in range(num_inputs):
@@ -439,16 +441,18 @@ class ShowAdmin(admin.ModelAdmin):
                 '''Save schedule'''
 
                 new_schedule = Schedule(pk=schedule_id,
-                                              rrule=rrule,
-                                              byweekday=byweekday,
-                                              show=show,
-                                              dstart=dstart,
-                                              tstart=tstart,
-                                              tend=tend,
-                                              until=until,
-                                              is_repetition=is_repetition,
-                                              automation_id=automation_id,
-                                              fallback_id=fallback_id)
+                                        rrule=rrule,
+                                        byweekday=byweekday,
+                                        show=show,
+                                        dstart=dstart,
+                                        tstart=tstart,
+                                        tend=tend,
+                                        until=until,
+                                        is_repetition=is_repetition,
+                                        automation_id=automation_id,
+                                        fallback_id=fallback_id,
+                                        add_days_no=add_days_no,
+                                        add_business_days_only=add_business_days_only)
 
                 # Only save schedule if any timeslots changed
                 if len(resolved_timeslots) > 0:
diff --git a/program/migrations/0017_auto_20180314_1409.py b/program/migrations/0017_auto_20180314_1409.py
new file mode 100644
index 00000000..2dc44cba
--- /dev/null
+++ b/program/migrations/0017_auto_20180314_1409.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.3 on 2018-03-14 14:09
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('program', '0016_auto_20180222_1253'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='schedule',
+            name='add_business_days_only',
+            field=models.BooleanField(default=False, verbose_name='Only add business days?'),
+        ),
+        migrations.AddField(
+            model_name='schedule',
+            name='add_days_no',
+            field=models.IntegerField(blank=True, null=True, verbose_name='Add days'),
+        ),
+    ]
diff --git a/program/models.py b/program/models.py
index 4a3fe716..486b3fb1 100644
--- a/program/models.py
+++ b/program/models.py
@@ -407,6 +407,8 @@ class Schedule(models.Model):
     tend = models.TimeField(_("End time"))
     until = models.DateField(_("Last date"))
     is_repetition = models.BooleanField(_("Is repetition"), default=False)
+    add_days_no = models.IntegerField(_("Add days"), blank=True, null=True)
+    add_business_days_only = models.BooleanField(_("Only add business days?"), default=False)
     fallback_id = models.IntegerField(_("Fallback ID"), blank=True, null=True)
     automation_id = models.IntegerField(_("Automation ID"), blank=True, null=True, choices=get_automation_id_choices()) # Deprecated
     created = models.DateTimeField(auto_now_add=True, editable=False, null=True) #-> both see https://stackoverflow.com/questions/1737017/django-auto-now-and-auto-now-add
@@ -440,6 +442,8 @@ class Schedule(models.Model):
         is_repetition = True if 'is_repetition' in sdl and sdl['is_repetition'] == 'true' else False
         fallback_id = int(sdl['fallback_id']) if sdl['fallback_id'] else None
         automation_id = int(sdl['automation_id']) if sdl['automation_id'] else None
+        add_days_no = int(sdl['add_days_no']) if sdl['add_days_no'] > 0 else None
+        add_business_days_only = True if sdl['add_business_days_only'] == 'true' else False
 
         dstart = datetime.strptime(str(sdl['dstart']), '%Y-%m-%d').date()
 
@@ -468,7 +472,8 @@ class Schedule(models.Model):
         schedule = Schedule(pk=pk, byweekday=sdl['byweekday'], rrule=rrule,
                             dstart=dstart, tstart=tstart, tend=tend,
                             until=until, is_repetition=is_repetition,
-                            fallback_id=fallback_id, show=show)
+                            fallback_id=fallback_id, show=show,
+                            add_days_no=add_days_no, add_business_days_only=add_business_days_only)
 
         return schedule
 
@@ -553,6 +558,52 @@ class Schedule(models.Model):
                           byweekno=byweekno_end))
 
         for k in range(min(len(starts), len(ends))):
+
+            # Correct dates for the (relatively seldom) case if:
+            # E.g.: 1st Monday from 23:00:00 to 1st Tuesday 00:00:00
+            #       produces wrong end dates if the 1st Tuesday is before the 1st Monday
+            #       In this case we take the next day instead of rrule's calculated end
+            if starts[k] > ends[k]:
+                ends[k] = datetime.combine(starts[k] + relativedelta(days=+1), schedule.tend)
+
+
+            '''
+            Add a number of days to the generated dates?
+
+            This can be helpful for repetitions:
+
+            Examples:
+
+              1. If RRule is "Every 1st Monday" and we want its repetition alyways to be on the following day,
+                 the repetition's RRule is the same but add_days_no is 1
+
+                 If we would set the repetition to "Every 1st Tuesday" instead
+                 we will get unmeant results if the 1st Tuesday is before the 1st Monday
+                 (e.g. 1st Tue = May 1 2018, 1st Mon = May 7 2018)
+
+              2. If RRule is "Every 1st Friday" and we want its repetition always to be on the following business day,
+                 the repetition's RRule is the same but add_days_no is 1 and add_business_days_only is True
+                 (e.g. original date = Fri, March 2 2018; generated date = Mon, March 5 2018)
+
+            In the UI these can be presets:
+                "On the following day" (add_days_no=1,add_business_days_only=False) or
+                "On the following business day" (add_days_no=1,add_business_days_only=True)
+
+            '''
+            if schedule.add_days_no != None and schedule.add_days_no > 0:
+                # If only business days and weekday is Fri, Sat or Sun: add add_days_no beginning from Sunday
+                weekday = datetime.date(starts[k]).weekday()
+                if schedule.add_business_days_only and weekday > 3:
+                    days_until_sunday = 6 - weekday
+                    starts[k] = starts[k] + relativedelta(days=+days_until_sunday+schedule.add_days_no)
+                    ends[k] = ends[k] + relativedelta(days=+days_until_sunday+schedule.add_days_no)
+                else:
+                    starts[k] = starts[k] + relativedelta(days=+schedule.add_days_no)
+                    ends[k] = ends[k] + relativedelta(days=+schedule.add_days_no)
+
+                if ends[k].date() > schedule.until:
+                    schedule.until = ends[k].date()
+
             timeslots.append(TimeSlot(schedule=schedule, start=starts[k], end=ends[k]).generate())
 
         return timeslots
@@ -576,7 +627,10 @@ class Schedule(models.Model):
                            ( Q(start__lte=ts.start) & Q(end__gte=ts.end) )
                         )
 
+            print("testing " + str(ts.start) + " - " + str(ts.end))
+
             if collision:
+                print("collision found: " + str(vars(collision[0])))
                 collisions.append(collision[0]) # TODO: Do we really always retrieve one?
             else:
                 collisions.append(None)
diff --git a/program/serializers.py b/program/serializers.py
index 696f8eed..0a5cb212 100644
--- a/program/serializers.py
+++ b/program/serializers.py
@@ -353,6 +353,8 @@ class ScheduleSerializer(serializers.ModelSerializer):
         instance.automation_id = validated_data.get('automation_id', instance.automation_id)
         instance.rrule = validated_data.get('rrule', instance.rrule)
         instance.show = validated_data.get('show', instance.show)
+        instance.add_days_no = validated_data.get('add_days_no', instance.add_days_no)
+        instance.add_business_days_only = validated_data.get('add_business_days_only', instance.add_business_days_only)
 
         instance.save()
         return instance
diff --git a/program/templates/boxes/broadcastformat.html b/program/templates/boxes/broadcastformat.html
deleted file mode 100644
index 8118cdb9..00000000
--- a/program/templates/boxes/broadcastformat.html
+++ /dev/null
@@ -1,11 +0,0 @@
-{% if broadcastformat_list %}
-    <dl id="broadcastformat" class="portlet">
-        <dt class="portletHeader"><span>Legende</span></dt>
-        {% for bf in broadcastformat_list %}
-            <dd class="portletItem bcformat bf-{{ bf.slug }}">
-                <a title="Sendungen mit dem Sendungsformat {{ bf.format }} anzeigen."
-                   href="?broadcastformat={{ bf.slug }}">{{ bf.format }}</a>
-            </dd>
-        {% endfor %}
-    </dl>
-{% endif %}
diff --git a/program/templates/collisions.html b/program/templates/collisions.html
index 24836912..c2ad336e 100644
--- a/program/templates/collisions.html
+++ b/program/templates/collisions.html
@@ -137,7 +137,9 @@
         <input type="hidden" name="ps_save_is_repetition" value="{{ schedule.is_repetition }}" />
         <input type="hidden" name="ps_save_automation_id" value="{{ schedule.automation_id }}" />
         <input type="hidden" name="ps_save_fallback_id" value="{{ schedule.fallback_id }}" />
-        <input type="hidden" name="ps_save_show_id" value="{{ schedule.show_id }} " />
+        <input type="hidden" name="ps_save_show_id" value="{{ schedule.show_id }}" />
+        <input type="hidden" name="ps_save_add_days_no" value="{{ schedule.add_days_no }}" />
+        <input type="hidden" name="ps_save_add_business_days_only" value="{{ schedule.add_business_days_only }}" />
         <input type="hidden" name="num_inputs" value="{{ num_inputs }}" />
         <input type="hidden" name="step" value="{{ step }}" />
 
diff --git a/program/templatetags/content_boxes.py b/program/templatetags/content_boxes.py
index 73fa691a..6f4caa32 100644
--- a/program/templatetags/content_boxes.py
+++ b/program/templatetags/content_boxes.py
@@ -14,14 +14,14 @@ def type():
 
 @register.inclusion_tag('boxes/musicfocus.html')
 def musicfocus():
-    return {'musicfocus_list': MusicFocus.objects.all()}
+    return {'musicfocus_list': MusicFocus.objects.filter(is_active=True)}
 
 
 @register.inclusion_tag('boxes/category.html')
 def category():
-    return {'category_list': Category.objects.all()}
+    return {'category_list': Category.objects.filter(is_active=True)}
 
 
 @register.inclusion_tag('boxes/topic.html')
 def topic():
-    return {'topic_list': Topic.objects.all()}
\ No newline at end of file
+    return {'topic_list': Topic.objects.filter(is_active=True)}
\ No newline at end of file
diff --git a/program/views.py b/program/views.py
index 7bebcfb4..538512ce 100644
--- a/program/views.py
+++ b/program/views.py
@@ -22,7 +22,6 @@ from program.models import Type, MusicFocus, Language, Note, Show, Category, Fun
 from program.serializers import TypeSerializer, LanguageSerializer, MusicFocusSerializer, NoteSerializer, ShowSerializer, ScheduleSerializer, CategorySerializer, FundingCategorySerializer, TopicSerializer, TimeSlotSerializer, HostSerializer, UserSerializer
 from program.utils import tofirstdayinisoweek, get_cached_shows
 
-
 # Deprecated
 class CalendarView(TemplateView):
     template_name = 'calendar.html'
@@ -203,6 +202,7 @@ class WeekScheduleView(TemplateView):
         return context
 
 
+# Deprecated
 class StylesView(TemplateView):
     template_name = 'styles.css'
     content_type = 'text/css'
@@ -255,6 +255,8 @@ def json_playout(request):
          If end not given, it returns all timeslots of the next 7 days
     """
 
+    from pv.settings import STATION_FALLBACK_ID
+
     if request.GET.get('start') == None:
         start = datetime.combine(date.today(), time(0, 0))
     else:
@@ -295,8 +297,8 @@ def json_playout(request):
             'schedule_id': ts.schedule.id,
             'is_repetition': ts.is_repetition,
             'playlist_id': ts.playlist_id,
-            'schedule_fallback_id': ts.schedule.fallback_id, # The schedule's fallback
-            'show_fallback_id': ts.show.fallback_id, # The show's fallback
+            'schedule_fallback_id': ts.schedule.fallback_id,
+            'show_fallback_id': ts.show.fallback_id,
             'show_id': ts.show.id,
             'show_name': ts.show.name + is_repetition,
             'show_hosts': hosts,
@@ -306,7 +308,7 @@ def json_playout(request):
             'show_musicfocus': musicfocus,
             'show_languages': languages,
             'show_fundingcategory': fundingcategory.fundingcategory,
-            'station_fallback_id': 0, # TODO: The station's global fallback (might change)
+            'station_fallback_id': STATION_FALLBACK_ID, # TODO: Find a better way than getting it from the settings
             'memo': ts.memo,
             'className': classname,
         }
diff --git a/pv/settings.py b/pv/settings.py
index ae747e6e..1ad5ba3b 100644
--- a/pv/settings.py
+++ b/pv/settings.py
@@ -144,6 +144,9 @@ MUSIKPROG_IDS = (
 )
 SPECIAL_PROGRAM_IDS = ()
 
+# The station's fallback playlist ID
+STATION_FALLBACK_ID = None
+
 # URL to CBA - Cultural Broadcasting Archive
 CBA_URL = 'https://cba.fro.at'
 
-- 
GitLab