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