From 822d5e0f1be523973a54c7b3cbd03829bcf318c2 Mon Sep 17 00:00:00 2001
From: ingo <ingo.leindecker@fro.at>
Date: Fri, 26 Jan 2018 18:48:52 +0100
Subject: [PATCH] Properly extend schedule and readme update

...as well as some minor changes and bug fixes.

See #8 #10 #22 #24
---
 README.rst                      | 60 +++++++++++++++++++++++----------
 frapp/views.py                  |  4 +--
 program/models.py               | 31 ++++++++++++-----
 program/templates/calendar.html |  4 ++-
 program/views.py                |  5 ++-
 pv/site_media/js/show_change.js |  7 ----
 6 files changed, 71 insertions(+), 40 deletions(-)

diff --git a/README.rst b/README.rst
index c586d643..a81cbece 100644
--- a/README.rst
+++ b/README.rst
@@ -1,57 +1,82 @@
-=================================
-Radio Helsinki Program Management
-=================================
+================================
+AURA Steering: Program Scheduler
+================================
 
 Installation
 ============
 
 To get setup you must have the following installed:
 
- * MySQL-Client Development libraries 
+ * MySQL-Client Development libraries
  * JPEG library development files
- * Python 2.7 including Development files
+ * Python 3.5 including Development files
  * virtualenv 1.11
 
 In Debian or Ubuntu (or derivatives) you should be able to achieve this with this command:
 
-    $ sudo apt-get install libmysqlclient-dev libjpeg-dev python2.7-dev virtualenv
+    $ sudo apt-get install libmysqlclient-dev libjpeg-dev python3.5-dev virtualenv
 
 
 Setting up the environment
 --------------------------
 
-Create a virtual environment where the dependencies will live::
+Create a virtual environment where the dependencies will live:
 
-    $ virtualenv -p python2.7 python
+    $ virtualenv -p python3.5 python
     $ source python/bin/activate
     (python)$
 
-Change into the base directory of this software and install the project dependencies::
+Change into the base directory of this software and install the project dependencies:
 
-    (python)$ pip install -r requirements.txt
+    (python)$ pip3 install -r requirements.txt
 
 
 Setting up the database
 -----------------------
 
-By default the project is set up to run on a SQLite 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 line 
+Create a file pv/local_settings.py and add at least the line
 
     SECRET_KEY = 'secret key'
 
-(obviously replacing "secret key" with a key of you choice).
+(obviously replacing "secret key" with a key of your choice).
 
-Then run::
+Then run:
 
     (python)$ python manage.py migrate
     (python)$ python manage.py loaddata program/fixtures/*.yaml
 
+### Setting up MySQL
+
+__Note:__ When adding your database, make sure you _don't_ use the collation _utf8mb4_unicode_ci_ or you will get a key length error during migration. (use e.g. _utf8_general_ci_ instead).
+
+To use MySQL, add the following to your local_settings.py (before migrating):
+
+    DATABASES = {
+        'default': {
+            'ENGINE': 'django.db.backends.mysql',
+            'OPTIONS': {
+                'read_default_file': os.path.join(PROJECT_DIR, 'mysql.cnf'),
+            },
+        }
+    }
+
+Create a file pv/mysql.cnf and give your MySQL credentials:
+
+    [client]
+    database =
+    host = localhost
+    port = 3309
+    user =
+    password =
+    default-character-set = utf8
+
 
 Adding an admin user
 --------------------
 
-In order to create an admin user (which you will need to login to the webinterface after the next step) run::
+In order to create an admin user (which you will need to login to the webinterface after the next step) run:
 
     (python)$ python manage.py createsuperuser
 
@@ -59,9 +84,8 @@ In order to create an admin user (which you will need to login to the webinterfa
 Running a web server
 --------------------
 
-In development you should run::
+In development you should run:
 
     (python)$ python manage.py runserver
 
-After this you can open http://127.0.0.1:8000/admin in your browser and log in with the admin credential you created previously.
-
+After this you can open http://127.0.0.1:8000/admin in your browser and log in with the admin credential you created previously.
\ No newline at end of file
diff --git a/frapp/views.py b/frapp/views.py
index 48026687..dc6da53e 100644
--- a/frapp/views.py
+++ b/frapp/views.py
@@ -102,8 +102,8 @@ def json_frapp(request):
         metainfos.append({ 'key': 'ProduzentIn', 'value': ', '.join(ts.show.hosts.values_list('name', flat=True)) })
         metainfos.append({ 'key': 'E-Mail', 'value': ', '.join(ts.show.hosts.values_list('email', flat=True)) })
 
-        image = '' if s.image.name == None else str(get_current_site(request)) + MEDIA_URL + s.image.name
-        url = '' if s.website == None else s.website
+        image = '' if s.image.name == None or s.image.name == '' else str(get_current_site(request)) + MEDIA_URL + s.image.name
+        url = '' if s.website == None or s.website == '' else s.website
 
         # Get active schedules for the given date
         # But include upcoming single timeslots (with rrule_id=1)
diff --git a/program/models.py b/program/models.py
index 397ae116..0c7371d1 100644
--- a/program/models.py
+++ b/program/models.py
@@ -7,7 +7,6 @@ from django.forms.models import model_to_dict
 from django.utils.translation import ugettext_lazy as _
 from versatileimagefield.fields import VersatileImageField, PPOIField
 from django.conf import settings
-import hashlib
 
 from tinymce import models as tinymce_models
 
@@ -432,7 +431,7 @@ class Schedule(models.Model):
     def instantiate_upcoming(sdl, show_pk, pk=None):
         """Returns an upcoming schedule instance for conflict resolution"""
 
-        pk = int(show_pk) if pk != None else None
+        pk = int(pk) if pk != None else None
         rrule = RRule.objects.get(pk=int(sdl['rrule']))
         show = Show.objects.get(pk=int(show_pk))
 
@@ -441,7 +440,9 @@ class Schedule(models.Model):
         automation_id = int(sdl['automation_id']) if sdl['automation_id'] else None
 
         dstart = datetime.strptime(str(sdl['dstart']), '%Y-%m-%d').date()
-        if dstart < datetime.today().date(): # Schedule mustn't start in the past
+
+        # Schedule mustn't start in the past when adding
+        if pk == None and dstart < datetime.today().date():
             dstart = datetime.today().date()
 
         tstart = sdl['tstart'] + ':00' if len(str(sdl['tstart'])) == 5 else sdl['tstart']
@@ -731,11 +732,23 @@ class Schedule(models.Model):
         Returns conflicts dict
         """
 
-        # Generate schedule
+        # Generate schedule to be saved
         schedule = Schedule.instantiate_upcoming(sdl, show_pk, schedule_pk)
 
+        # Copy if dstart changes for generating timeslots
+        gen_schedule = schedule
+
         # Generate timeslots
-        timeslots = Schedule.generate_timeslots(schedule)
+
+        # If extending: Get last timeslot and start generating from that date on
+        if schedule_pk != None:
+            existing_schedule = Schedule.objects.get(pk=int(schedule_pk))
+
+            if schedule.until > existing_schedule.until:
+                last_timeslot = TimeSlot.objects.filter(schedule=existing_schedule).order_by('start').reverse()[0]
+                gen_schedule.dstart = last_timeslot.start.date() + timedelta(days=1)
+
+        timeslots = Schedule.generate_timeslots(gen_schedule)
 
         # Generate conflicts and add schedule
         conflicts = Schedule.generate_conflicts(timeslots)
@@ -759,7 +772,7 @@ class Schedule(models.Model):
         # Regenerate conflicts
         schedule = Schedule.instantiate_upcoming(sdl, show_pk, schedule_pk)
         show = schedule.show
-        conflicts = Schedule.make_conflicts(sdl, schedule.pk, show.pk)
+        conflicts = Schedule.make_conflicts(sdl, schedule_pk, show_pk)
 
         if schedule.rrule.freq > 0 and schedule.dstart == schedule.until:
             return {'detail': _("Start and until dates mustn't be the same")}
@@ -767,7 +780,7 @@ class Schedule(models.Model):
         if schedule.until < schedule.dstart:
             return {'detail': _("Until date mustn't before start")}
 
-        num_conflicts = len([pr for pr in conflicts['projected'] if len(x['collisions']) > 0])
+        num_conflicts = len([pr for pr in conflicts['projected'] if len(pr['collisions']) > 0])
 
         if len(solutions) != num_conflicts:
             return {'detail': _("Numbers of conflicts and solutions don't match.")}
@@ -976,7 +989,7 @@ class Schedule(models.Model):
 
         # Only save schedule if timeslots were created
         if create:
-            # Create or save schedule
+            # Create or update schedule
             schedule.save()
 
             # Delete upcoming timeslots which still remain
@@ -1122,7 +1135,7 @@ class TimeSlot(models.Model):
         self.show = self.schedule.show
 
         # Generate a distinct and reproducible hash for the timeslot
-        # Makes sure none of these fields changed when updating
+        # Makes sure none of these fields changed when updating a schedule
         string = str(self.start) + str(self.end) + str(self.schedule.rrule.id) + str(self.schedule.byweekday)
         self.hash = str(''.join(s for s in string if s.isdigit()))
         return self
diff --git a/program/templates/calendar.html b/program/templates/calendar.html
index 6f76d79b..189d5fa4 100644
--- a/program/templates/calendar.html
+++ b/program/templates/calendar.html
@@ -110,6 +110,8 @@
         <div id="show-type"></div>
         <div id="show-musicfocus"></div>
         <div id="show-rtrcategory"></div>
+        <p></p>
+        <div id="memo"></div>
         <div id="response-message"></div>
       </div>
     </div>
@@ -225,7 +227,7 @@
             jQuery("#show-categories").html('Categories: ' + calEvent.show_categories);
             jQuery("#show-topics").html('Topics: ' + calEvent.show_topics);
             jQuery("#show-musicfocus").html('Music focus: ' + calEvent.show_musicfocus);
-            jQuery("#is-repetition").html('WH: ' + calEvent.is_repetition);
+            jQuery("#is-repetition").html('Is repetition: ' + calEvent.is_repetition);
             jQuery("#playlist-id").html('Playlist ID: ' + calEvent.playlist_id);
             jQuery("#fallback-id").html('Fallback ID: ' + calEvent.fallback_id);
             jQuery("#memo").html(calEvent.memo);
diff --git a/program/views.py b/program/views.py
index f83da008..b191bfb7 100644
--- a/program/views.py
+++ b/program/views.py
@@ -599,9 +599,7 @@ class APIScheduleViewSet(viewsets.ModelViewSet):
         Create a schedule, generate timeslots, test for collisions and resolve them including notes
 
         Only superusers may add schedules
-        TODO: if nothing changed except for is_repetition, fallback_id or automation_id
-        TODO: Prolonging a schedule properly withouth matching against itself
-        + Perhaps directly insert into database if no conflicts found
+        TODO: Perhaps directly insert into database if no conflicts found
         """
 
         # Only allow creating when calling /shows/1/schedules/
@@ -646,6 +644,7 @@ class APIScheduleViewSet(viewsets.ModelViewSet):
 
         # First update submit -> return projected timeslots and collisions
         if not 'solutions' in request.data:
+            # TODO: If nothing else than fallback_id, automation_id or is_repetition changed -> just save and don't do anything
             return Response(Schedule.make_conflicts(request.data['schedule'], pk, show_pk))
 
         # Otherwise try to resolve
diff --git a/pv/site_media/js/show_change.js b/pv/site_media/js/show_change.js
index 5e3f8f2c..52c3bf69 100644
--- a/pv/site_media/js/show_change.js
+++ b/pv/site_media/js/show_change.js
@@ -52,8 +52,6 @@ django.jQuery(document).ready( function() {
 		var ps_id = django.jQuery(this).closest('tr').attr("id");
       var dstart = django.jQuery(this).val();
 
-      //django.jQuery(this).removeClass('validation-error');
-
    	/* Set the until date to dstart if editing a programslot with freq 'once' */
 		if( django.jQuery('#id_' + ps_id + '-rrule option:selected').val() == 1 ) {
    		django.jQuery('#id_' + ps_id + '-until').show().val(dstart).hide();
@@ -92,11 +90,6 @@ django.jQuery(document).ready( function() {
                    alert('Enddatum darf nicht vor dem Startdatum liegen');
                    django.jQuery(this).addClass('validation-error');
                }
-
-           } else if( until == '' && dstart != '') {
-               e.preventDefault();
-               alert('Enddatum darf nicht leer sein');
-               django.jQuery(this).addClass('validation-error');
            } else if( dstart == '' && until != '') {
            	   e.preventDefault();
            	   alert('Startdatum darf nicht leer sein');
-- 
GitLab