From 4d482470ec1ba9b9852e691586aa9014dea2b1c8 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Fri, 17 Nov 2017 14:52:54 -0500 Subject: [PATCH 01/31] Allow further configuration of VM mount options Allow for control of whether NFS is used, which version of NFS to use, and for setting other VM mount options via envirionment variables. Fixes migrations not running on provision on ubuntu 17.10. --- Vagrantfile | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index c7d908343..989983213 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -4,11 +4,28 @@ Vagrant.require_version ">= 1.5" require "yaml" +CAC_SHARED_FOLDER_TYPE = ENV.fetch("CAC_SHARED_FOLDER_TYPE", "nfs") +CAC_NFS_VERSION = ENV.fetch("CAC_NFS_VERSION_3", true) ? 'vers=3': 'vers=4' + +if CAC_SHARED_FOLDER_TYPE == "nfs" + if Vagrant::Util::Platform.linux? then + CAC_MOUNT_OPTIONS = ['rw', CAC_NFS_VERSION, 'tcp', 'nolock'] + else + CAC_MOUNT_OPTIONS = [CAC_NFS_VERSION, 'udp'] + end +else + if ENV.has_key?("CAC_MOUNT_OPTIONS") + CAC_MOUNT_OPTIONS = ENV.fetch("CAC_MOUNT_OPTIONS").split + else + CAC_MOUNT_OPTIONS = ["rw"] + end +end + if ENV['CAC_TRIPPLANNER_MEMORY'].nil? # OpenTripPlanner needs > 1GB to build and run - OTP_MEMORY_MB = "4096" + CAC_MEMORY_MB = "4096" else - OTP_MEMORY_MB = ENV['CAC_TRIPPLANNER_MEMORY'] + CAC_MEMORY_MB = ENV['CAC_TRIPPLANNER_MEMORY'] end if ENV['CAC_TRIPPLANNER_CPU'].nil? @@ -118,11 +135,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| if testing? app.vm.synced_folder ".", "/opt/app" else - app.vm.synced_folder ".", "/opt/app", :nfs => true, :mount_options => [ - ("nfsvers=3" if ENV.fetch("CAC_NFS_VERSION_3", false)), - "noatime", - "actimeo=1", - ] + app.vm.synced_folder ".", "/opt/app", type: CAC_SHARED_FOLDER_TYPE, mount_options: CAC_MOUNT_OPTIONS end # Web @@ -170,7 +183,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| end otp.vm.provider :virtualbox do |v| - v.memory = OTP_MEMORY_MB + v.memory = CAC_MEMORY_MB v.cpus = CPUS end end From def49caab7321b3f3146bdd814d038b8b279a4b2 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Fri, 17 Nov 2017 17:11:56 -0500 Subject: [PATCH 02/31] Upgrade to Django 1.9 No changes to app itself. Minor changes to wp-admin plugin, pulled from an upstream PR. --- .../ansible/roles/cac-tripplanner.app/defaults/main.yml | 2 +- deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/deployment/ansible/roles/cac-tripplanner.app/defaults/main.yml b/deployment/ansible/roles/cac-tripplanner.app/defaults/main.yml index 08a202b8e..5021b5466 100644 --- a/deployment/ansible/roles/cac-tripplanner.app/defaults/main.yml +++ b/deployment/ansible/roles/cac-tripplanner.app/defaults/main.yml @@ -22,7 +22,7 @@ app_cron_event_feed: "cd {{ root_app_dir }} && python manage.py load_events >> { cac_python_dependencies: - { name: 'base58', version: '0.2.5' } - { name: 'boto', version: '2.48.0' } - - { name: 'django', version: '1.8.18' } + - { name: 'django', version: '1.9.13' } - { name: 'django-ckeditor', version: '5.3.0' } - { name: 'django-extensions', version: '1.9.1' } - { name: 'django-storages', version: '1.6.5' } diff --git a/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml b/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml index e3ddecce6..4d9ed58b3 100644 --- a/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml +++ b/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml @@ -16,7 +16,9 @@ # Putting 'editable: false' in the entry in cac_python_dependencies should make it install # non-editable, but it's getting ignored - name: Install django-wpadmin manually to work around ansible bug - command: pip install 'git+https://github.com/azavea/django-wpadmin@v1.8.13#egg=django-wpadmin' + command: pip install 'git+https://github.com/flibbertigibbet/django-wpadmin@feature/django-1-9#egg=django-wpadmin' + # FIXME: remove + #command: pip install 'git+https://github.com/azavea/django-wpadmin@v1.8.13#egg=django-wpadmin' # TODO: peg this to a version, rather than a commit, when released # ansible pip module installs this in /tmp/src for some reason, so we use the From a41b4db43790f18f597e67713f7c10ddbe807311 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Fri, 17 Nov 2017 17:14:20 -0500 Subject: [PATCH 03/31] Minor pip package version upgrades --- .../ansible/roles/cac-tripplanner.app/defaults/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deployment/ansible/roles/cac-tripplanner.app/defaults/main.yml b/deployment/ansible/roles/cac-tripplanner.app/defaults/main.yml index 5021b5466..3ac80ad50 100644 --- a/deployment/ansible/roles/cac-tripplanner.app/defaults/main.yml +++ b/deployment/ansible/roles/cac-tripplanner.app/defaults/main.yml @@ -23,13 +23,13 @@ cac_python_dependencies: - { name: 'base58', version: '0.2.5' } - { name: 'boto', version: '2.48.0' } - { name: 'django', version: '1.9.13' } - - { name: 'django-ckeditor', version: '5.3.0' } - - { name: 'django-extensions', version: '1.9.1' } + - { name: 'django-ckeditor', version: '5.3.1' } + - { name: 'django-extensions', version: '1.9.7' } - { name: 'django-storages', version: '1.6.5' } - { name: 'gunicorn', version: '19.7.1' } - { name: 'pillow', version: '4.3.0' } - { name: 'psycopg2', version: '2.7.3' } - - { name: 'pytz', version: '2017.2' } + - { name: 'pytz', version: '2017.3' } - { name: 'pyyaml', version: '3.12' } - { name: 'troposphere', version: '0.7.2'} # Note: django-wpadmin is installed manually to work around the fact that ansible-pip From 47a22d0ac60b0c2aa81174a22e22c9d48c01ed51 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Mon, 20 Nov 2017 12:40:18 -0500 Subject: [PATCH 04/31] Handle django-wpadmin version upgrades on provision Upgrade package if installation already exists. --- deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml b/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml index 4d9ed58b3..75d00ae66 100644 --- a/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml +++ b/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml @@ -16,7 +16,7 @@ # Putting 'editable: false' in the entry in cac_python_dependencies should make it install # non-editable, but it's getting ignored - name: Install django-wpadmin manually to work around ansible bug - command: pip install 'git+https://github.com/flibbertigibbet/django-wpadmin@feature/django-1-9#egg=django-wpadmin' + command: pip install --upgrade 'git+https://github.com/flibbertigibbet/django-wpadmin@feature/django-1-9#egg=django-wpadmin' # FIXME: remove #command: pip install 'git+https://github.com/azavea/django-wpadmin@v1.8.13#egg=django-wpadmin' From 5490e10920fd38a4089ac1eb5efdb3f828978a1c Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Mon, 20 Nov 2017 13:22:02 -0500 Subject: [PATCH 05/31] Use 1.9.x django-wpadmin release --- deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml b/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml index 75d00ae66..ac9bcce11 100644 --- a/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml +++ b/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml @@ -16,9 +16,7 @@ # Putting 'editable: false' in the entry in cac_python_dependencies should make it install # non-editable, but it's getting ignored - name: Install django-wpadmin manually to work around ansible bug - command: pip install --upgrade 'git+https://github.com/flibbertigibbet/django-wpadmin@feature/django-1-9#egg=django-wpadmin' - # FIXME: remove - #command: pip install 'git+https://github.com/azavea/django-wpadmin@v1.8.13#egg=django-wpadmin' + command: pip install --upgrade 'git+https://github.com/azavea/django-wpadmin@v1.9#egg=django-wpadmin' # TODO: peg this to a version, rather than a commit, when released # ansible pip module installs this in /tmp/src for some reason, so we use the From e8e6dbc9648544d49bccb75c0700bb728e07a1e3 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Tue, 21 Nov 2017 10:59:36 -0500 Subject: [PATCH 06/31] Upgrade to Django 1.10 Bump django version and django-wpadmin version. --- .../ansible/roles/cac-tripplanner.app/defaults/main.yml | 2 +- deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/deployment/ansible/roles/cac-tripplanner.app/defaults/main.yml b/deployment/ansible/roles/cac-tripplanner.app/defaults/main.yml index 3ac80ad50..a7d564ae9 100644 --- a/deployment/ansible/roles/cac-tripplanner.app/defaults/main.yml +++ b/deployment/ansible/roles/cac-tripplanner.app/defaults/main.yml @@ -22,7 +22,7 @@ app_cron_event_feed: "cd {{ root_app_dir }} && python manage.py load_events >> { cac_python_dependencies: - { name: 'base58', version: '0.2.5' } - { name: 'boto', version: '2.48.0' } - - { name: 'django', version: '1.9.13' } + - { name: 'django', version: '1.10.8' } - { name: 'django-ckeditor', version: '5.3.1' } - { name: 'django-extensions', version: '1.9.7' } - { name: 'django-storages', version: '1.6.5' } diff --git a/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml b/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml index ac9bcce11..d2b609301 100644 --- a/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml +++ b/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml @@ -16,7 +16,9 @@ # Putting 'editable: false' in the entry in cac_python_dependencies should make it install # non-editable, but it's getting ignored - name: Install django-wpadmin manually to work around ansible bug - command: pip install --upgrade 'git+https://github.com/azavea/django-wpadmin@v1.9#egg=django-wpadmin' + command: pip install --upgrade 'git+https://github.com/flibbertigibbet/django-wpadmin@feature/django-1-11#egg=django-wpadmin' + # TODO: change to release install + #command: pip install --upgrade 'git+https://github.com/azavea/django-wpadmin@v1.9#egg=django-wpadmin' # TODO: peg this to a version, rather than a commit, when released # ansible pip module installs this in /tmp/src for some reason, so we use the From f4e0eb17d538e5458601ccf280c234e5c9b12a6a Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Tue, 21 Nov 2017 11:39:58 -0500 Subject: [PATCH 07/31] Upgrade to Django 1.11 --- deployment/ansible/roles/cac-tripplanner.app/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/ansible/roles/cac-tripplanner.app/defaults/main.yml b/deployment/ansible/roles/cac-tripplanner.app/defaults/main.yml index a7d564ae9..f55b55de6 100644 --- a/deployment/ansible/roles/cac-tripplanner.app/defaults/main.yml +++ b/deployment/ansible/roles/cac-tripplanner.app/defaults/main.yml @@ -22,7 +22,7 @@ app_cron_event_feed: "cd {{ root_app_dir }} && python manage.py load_events >> { cac_python_dependencies: - { name: 'base58', version: '0.2.5' } - { name: 'boto', version: '2.48.0' } - - { name: 'django', version: '1.10.8' } + - { name: 'django', version: '1.11.7' } - { name: 'django-ckeditor', version: '5.3.1' } - { name: 'django-extensions', version: '1.9.7' } - { name: 'django-storages', version: '1.6.5' } From e0cfff7625dcf16cf49b6da5490612ef7c626508 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Tue, 21 Nov 2017 13:55:09 -0500 Subject: [PATCH 08/31] Use release version of django-wpadmin --- deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml b/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml index d2b609301..db90cb92b 100644 --- a/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml +++ b/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml @@ -16,10 +16,9 @@ # Putting 'editable: false' in the entry in cac_python_dependencies should make it install # non-editable, but it's getting ignored - name: Install django-wpadmin manually to work around ansible bug - command: pip install --upgrade 'git+https://github.com/flibbertigibbet/django-wpadmin@feature/django-1-11#egg=django-wpadmin' - # TODO: change to release install - #command: pip install --upgrade 'git+https://github.com/azavea/django-wpadmin@v1.9#egg=django-wpadmin' + command: pip install --upgrade 'git+https://github.com/azavea/django-wpadmin@v1.11#egg=django-wpadmin' +# TODO: #914 replace major kirby # TODO: peg this to a version, rather than a commit, when released # ansible pip module installs this in /tmp/src for some reason, so we use the # command instead From ac750c64e093e886f1b1a57e299df5fe4f00eb43 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Tue, 21 Nov 2017 14:20:43 -0500 Subject: [PATCH 09/31] Update npm dependencies Fixes turf point-on-line package warning about package rename. Also updates a few other packages. Fixes #915. --- src/gulpfile.js | 2 +- src/package.json | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/gulpfile.js b/src/gulpfile.js index 87357a8ef..addba148b 100644 --- a/src/gulpfile.js +++ b/src/gulpfile.js @@ -98,7 +98,7 @@ var buildTurfHelpers = function() { }; var buildTurfPointOnLine = function() { - return browserify(turfRoot + 'point-on-line', { + return browserify(turfRoot + 'nearest-point-on-line', { standalone: 'turf.pointOnLine', exclude: [turfRoot + 'helpers'] }) diff --git a/src/package.json b/src/package.json index 7db67507c..000180a44 100644 --- a/src/package.json +++ b/src/package.json @@ -4,13 +4,13 @@ "node": ">=0.12.0" }, "dependencies": { - "@turf/point-on-line": "~5.0.3" + "@turf/nearest-point-on-line": "~5.0.4" }, "devDependencies": { "aliasify": "^2.0.0", - "apache-server-configs": "~2.14.0", + "apache-server-configs": "~2.15.0", "bower": "~1.8.2", - "browserify": "~14.4.0", + "browserify": "~14.5.0", "chai": "~4.1.2", "connect": "~3.6.5", "connect-livereload": "~0.6.0", @@ -18,14 +18,14 @@ "gulp": "~3.9.1", "gulp-add-src": "~0.2.0", "gulp-autoprefixer": "~4.0.0", - "gulp-cache": "~0.4.5", + "gulp-cache": "~1.0.1", "gulp-concat": "~2.6.1", "gulp-csso": "~3.0.0", "gulp-debug": "~3.1.0", "gulp-filter": "~5.0.1", "gulp-flatten": "~0.3.1", "gulp-if": "~2.0.2", - "gulp-imagemin": "~3.4.0", + "gulp-imagemin": "~4.0.0", "gulp-jshint": "~2.0.4", "gulp-jshint-xml-file-reporter": "~0.5.1", "gulp-load-plugins": "~1.5.0", @@ -54,7 +54,7 @@ "merge-stream": "~1.0.0", "mocha": "~4.0.1", "opn": "~5.1.0", - "phantomjs-prebuilt": "^2.1.15", + "phantomjs-prebuilt": "^2.1.16", "pump": "~1.0.2", "requirejs": "~2.3.5", "serve-index": "~1.9.1", From dcd95fe41994f02f08723f1ed94cd740e63f4527 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Wed, 22 Nov 2017 18:30:38 -0500 Subject: [PATCH 10/31] Use TemplatesSetting renderer Fixes issue https://code.djangoproject.com/ticket/28074 where template not found on destination edit/add. --- python/cac_tripplanner/cac_tripplanner/settings.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/python/cac_tripplanner/cac_tripplanner/settings.py b/python/cac_tripplanner/cac_tripplanner/settings.py index 99e17b933..bab7472a4 100644 --- a/python/cac_tripplanner/cac_tripplanner/settings.py +++ b/python/cac_tripplanner/cac_tripplanner/settings.py @@ -7,6 +7,7 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.7/ref/settings/ """ +import django from boto.utils import get_instance_metadata from django.core.exceptions import ImproperlyConfigured @@ -223,12 +224,20 @@ } # TEMPLATE CONFIGURATION -# See https://docs.djangoproject.com/en/1.8/ref/settings/#templates +# See https://docs.djangoproject.com/en/1.11/ref/settings/#templates + +# set renderer +# https://docs.djangoproject.com/en/1.11/ref/forms/renderers/#django.forms.renderers.TemplatesSetting +FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' + TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ os.path.normpath(os.path.join(BASE_DIR, 'templates')), + 'django/forms/templates', + 'templates', + os.path.normpath(os.path.join(django.__path__[0] + '/forms/templates')) ], 'APP_DIRS': True, 'OPTIONS': { From 3548524f733b9c47b9a8964f707b630c4410553c Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Tue, 28 Nov 2017 12:14:28 -0500 Subject: [PATCH 11/31] Remove ckeditor jquery setting As of version 5.3.0, django-ckeditor no longer has a jquery dependency. https://github.com/django-ckeditor/django-ckeditor/blob/3f40dec1512ab0cc18fbac420649988b24d27328/CHANGELOG.rst#530 --- python/cac_tripplanner/cac_tripplanner/settings.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/python/cac_tripplanner/cac_tripplanner/settings.py b/python/cac_tripplanner/cac_tripplanner/settings.py index bab7472a4..fb7d39110 100644 --- a/python/cac_tripplanner/cac_tripplanner/settings.py +++ b/python/cac_tripplanner/cac_tripplanner/settings.py @@ -259,9 +259,6 @@ CKEDITOR_UPLOAD_PATH = 'uploads/' CKEDITOR_IMAGE_BACKEND = 'pillow' -# TODO: delete later. -CKEDITOR_JQUERY_URL = '//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js' - CKEDITOR_CONFIGS = { 'default': { 'toolbar': [ From 2ecee3e4ead9d1337347ae102a34158cbc6d8605 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Tue, 28 Nov 2017 12:42:29 -0500 Subject: [PATCH 12/31] Add events to CMS Add event type to CMS destinations app. Also remove openlayers js override (no longer needed; default is now SSL) and update version of jquery for geocoder to match that used by front end app. --- python/cac_tripplanner/destinations/admin.py | 31 +++++++++++----- python/cac_tripplanner/destinations/forms.py | 9 ++++- .../destinations/migrations/0021_event.py | 37 +++++++++++++++++++ python/cac_tripplanner/destinations/models.py | 37 ++++++++++++++++++- 4 files changed, 103 insertions(+), 11 deletions(-) create mode 100644 python/cac_tripplanner/destinations/migrations/0021_event.py diff --git a/python/cac_tripplanner/destinations/admin.py b/python/cac_tripplanner/destinations/admin.py index 11ec1d382..52078587e 100644 --- a/python/cac_tripplanner/destinations/admin.py +++ b/python/cac_tripplanner/destinations/admin.py @@ -1,11 +1,11 @@ from django.conf import settings -from django.contrib.gis import admin +from django.contrib import admin, gis -from .forms import DestinationForm -from .models import Destination +from .forms import DestinationForm, EventForm +from .models import Destination, Event -class DestinationAdmin(admin.OSMGeoAdmin): +class DestinationAdmin(gis.admin.OSMGeoAdmin): form = DestinationForm list_display = ('name', 'published', 'priority', 'address', 'city', 'state', 'zip') @@ -18,12 +18,8 @@ class DestinationAdmin(admin.OSMGeoAdmin): # Override map_template for custom address geocoding behavior map_template = 'admin/cac-geocoding-map.html' - # Override configurable URL for openlayers.js to support SSL - # default is: 'http://openlayers.org/api/2.13.1/OpenLayers.js' - openlayers_url = 'https://cdnjs.cloudflare.com/ajax/libs/openlayers/2.13.1/OpenLayers.js' - # Include geocoder dependencies - jquery = 'https://code.jquery.com/jquery-2.1.3.min.js' + jquery = 'https://code.jquery.com/jquery-3.2.1.min.js' if settings.DEBUG: extra_js = [ jquery, @@ -48,4 +44,21 @@ def make_unpublished(self, request, queryset): make_unpublished.short_description = 'Unpublish selected destinations' +class EventAdmin(admin.ModelAdmin): + form = EventForm + + list_display = ('name', 'published', 'priority', ) + actions = ('make_published', 'make_unpublished', ) + ordering = ('name', ) + + def make_published(self, request, queryset): + queryset.update(published=True) + make_published.short_description = 'Publish selected events' + + def make_unpublished(self, request, queryset): + queryset.update(published=False) + make_unpublished.short_description = 'Unpublish selected events' + + admin.site.register(Destination, DestinationAdmin) +admin.site.register(Event, EventAdmin) diff --git a/python/cac_tripplanner/destinations/forms.py b/python/cac_tripplanner/destinations/forms.py index 1a9d8b347..59c884db6 100644 --- a/python/cac_tripplanner/destinations/forms.py +++ b/python/cac_tripplanner/destinations/forms.py @@ -1,6 +1,6 @@ from django.forms import ModelForm -from .models import Destination +from .models import Destination, Event from cac_tripplanner.image_utils import validate_image @@ -17,3 +17,10 @@ def clean_image(self): def clean_wide_image(self): """Custom validator for wide_image field""" return validate_image(self.cleaned_data.get('wide_image', False), 680, 400) + + +class EventForm(DestinationForm): + """Validate image dimensions""" + class Meta: + model = Event + exclude = [] diff --git a/python/cac_tripplanner/destinations/migrations/0021_event.py b/python/cac_tripplanner/destinations/migrations/0021_event.py new file mode 100644 index 000000000..f488b9695 --- /dev/null +++ b/python/cac_tripplanner/destinations/migrations/0021_event.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.7 on 2017-11-28 17:32 +from __future__ import unicode_literals + +import ckeditor.fields +import destinations.models +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('destinations', '0020_auto_20170203_1251'), + ] + + operations = [ + migrations.CreateModel( + name='Event', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50)), + ('website_url', models.URLField(blank=True, null=True)), + ('description', ckeditor.fields.RichTextField()), + ('start_date', models.DateTimeField()), + ('end_date', models.DateTimeField()), + ('image', models.ImageField(help_text=b'The small image. Will be displayed at 310x155.', null=True, upload_to=destinations.models.generate_filename)), + ('wide_image', models.ImageField(help_text=b'The large image. Will be displayed at 680x400.', null=True, upload_to=destinations.models.generate_filename)), + ('published', models.BooleanField(default=False)), + ('priority', models.IntegerField(default=9999)), + ('destination', models.ForeignKey(null=True, blank=True, on_delete=django.db.models.deletion.SET_NULL, to='destinations.Destination')), + ], + options={ + 'ordering': ['priority', '-start_date'], + }, + ), + ] diff --git a/python/cac_tripplanner/destinations/models.py b/python/cac_tripplanner/destinations/models.py index ac66031d8..da1e67aff 100644 --- a/python/cac_tripplanner/destinations/models.py +++ b/python/cac_tripplanner/destinations/models.py @@ -12,11 +12,20 @@ def generate_filename(instance, filename): class DestinationManager(models.GeoManager): - """Custom manager for Destinations allows filtering on published""" + """Custom manager for Destinations that allows filtering on published""" def published(self): return self.get_queryset().filter(published=True) +class EventManager(DestinationManager): + """Custom manager for Events that allows filtering on published or currently ongoing""" + + def current(self): + return self.get_queryset().filter(end_date__gte=now(), start_date__lte=now()) + + def upcoming(self): + return self.get_queryset().filter(start_date__gt=now()) + class Destination(models.Model): """Represents a destination""" @@ -48,6 +57,32 @@ class Meta: def __unicode__(self): return self.name +class Event(models.Model): + """Represents an event, which has a start and end date""" + + class Meta: + ordering = ['priority', '-start_date'] + + name = models.CharField(max_length=50) + website_url = models.URLField(blank=True, null=True) + description = RichTextField() + start_date = models.DateTimeField() + end_date = models.DateTimeField() + + destination = models.ForeignKey('Destination', on_delete=models.SET_NULL, null=True, blank=True) + + image = models.ImageField(upload_to=generate_filename, null=True, + help_text='The small image. Will be displayed at 310x155.') + wide_image = models.ImageField(upload_to=generate_filename, null=True, + help_text='The large image. Will be displayed at 680x400.') + published = models.BooleanField(default=False) + priority = models.IntegerField(default=9999, null=False) + + objects = EventManager() + + def __unicode__(self): + return self.name + class FeedEventManager(models.GeoManager): """Custom manager for FeedEvents allows filtering on publication_date""" From c6c3d983c04715e8a979fbb527725d2a2585e5db Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Tue, 28 Nov 2017 13:00:54 -0500 Subject: [PATCH 13/31] Add event tests --- python/cac_tripplanner/destinations/tests.py | 38 +++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/python/cac_tripplanner/destinations/tests.py b/python/cac_tripplanner/destinations/tests.py index aeea13de2..81469b867 100644 --- a/python/cac_tripplanner/destinations/tests.py +++ b/python/cac_tripplanner/destinations/tests.py @@ -6,7 +6,43 @@ from django.test import Client, TestCase from django.utils.timezone import now -from destinations.models import Destination, FeedEvent +from destinations.models import Destination, Event, FeedEvent + + +class EventTests(TestCase): + def setUp(self): + # Clear DB of objects created by migrations + Event.objects.all().delete() + + test_image = File(open('default_media/square/BartramsGarden.jpg')) + + self.now = now() + + common_args = dict( + description='Sample event for tests', + image=test_image, + wide_image=test_image + ) + + self.client = Client() + + self.event_1 = Event.objects.create(name='Current Event', + published=True, + start_date=self.now, + end_date=self.now + timedelta(days=1), + **common_args) + + self.event_2 = Event.objects.create(name='Unpublished Past Event', + published=False, + start_date=self.now - timedelta(days=7), + end_date=self.now - timedelta(days=2), + **common_args) + + def test_event_manager_published(self): + self.assertEqual(Event.objects.published().count(), 1) + + def test_event_manager_current(self): + self.assertEqual(Event.objects.current().count(), 1) class DestinationTests(TestCase): From 360af02c40a6cafa04b251ff20480dceaf131c76 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Tue, 28 Nov 2017 13:22:16 -0500 Subject: [PATCH 14/31] Add event date validation Start date must be before end date. --- python/cac_tripplanner/destinations/forms.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/python/cac_tripplanner/destinations/forms.py b/python/cac_tripplanner/destinations/forms.py index 59c884db6..f95217001 100644 --- a/python/cac_tripplanner/destinations/forms.py +++ b/python/cac_tripplanner/destinations/forms.py @@ -1,4 +1,4 @@ -from django.forms import ModelForm +from django.forms import ModelForm, ValidationError from .models import Destination, Event from cac_tripplanner.image_utils import validate_image @@ -24,3 +24,14 @@ class EventForm(DestinationForm): class Meta: model = Event exclude = [] + + def clean(self): + """Validate start date is less than end date""" + cleaned_data = super(EventForm, self).clean() + start = self.cleaned_data.get('start_date') + end = self.cleaned_data.get('end_date') + + if start and end and start >= end: + self.add_error('start_date', ValidationError('Start date must be before end date.')) + + return cleaned_data From 369b199745db2ee67bbca1d3f8aa2bf248b151ec Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Wed, 29 Nov 2017 16:08:29 -0500 Subject: [PATCH 15/31] Set verbose app name Change the app name for the `destinations` django app as displayed in the admin panel. See: https://docs.djangoproject.com/en/1.11/ref/applications/#for-application-authors --- python/cac_tripplanner/destinations/__init__.py | 1 + python/cac_tripplanner/destinations/apps.py | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 python/cac_tripplanner/destinations/apps.py diff --git a/python/cac_tripplanner/destinations/__init__.py b/python/cac_tripplanner/destinations/__init__.py index e69de29bb..ea21cb168 100644 --- a/python/cac_tripplanner/destinations/__init__.py +++ b/python/cac_tripplanner/destinations/__init__.py @@ -0,0 +1 @@ +default_app_config = 'destinations.apps.DestinationsConfig' diff --git a/python/cac_tripplanner/destinations/apps.py b/python/cac_tripplanner/destinations/apps.py new file mode 100644 index 000000000..2fa50211e --- /dev/null +++ b/python/cac_tripplanner/destinations/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + +class DestinationsConfig(AppConfig): + name = 'destinations' + verbose_name = 'Destinations and Events' From 1ad66ac0085a01a52d1c01e6a0f6129cc63fc486 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Wed, 29 Nov 2017 17:31:36 -0500 Subject: [PATCH 16/31] Disallow related destination edit Override default admin form override to prevent display of add/edit/delete buttons that open a popup for making modifications to the related model. --- python/cac_tripplanner/destinations/forms.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/python/cac_tripplanner/destinations/forms.py b/python/cac_tripplanner/destinations/forms.py index f95217001..3428d3dc3 100644 --- a/python/cac_tripplanner/destinations/forms.py +++ b/python/cac_tripplanner/destinations/forms.py @@ -1,3 +1,4 @@ +from django.contrib.admin import widgets from django.forms import ModelForm, ValidationError from .models import Destination, Event @@ -6,6 +7,7 @@ class DestinationForm(ModelForm): """Validate image dimensions""" + class Meta: model = Destination exclude = [] @@ -20,11 +22,21 @@ def clean_wide_image(self): class EventForm(DestinationForm): - """Validate image dimensions""" + """Admin form for editing events + + Subclasses destination form for image validation. + """ + class Meta: model = Event exclude = [] + def __init__(self, *args, **kwargs): + super(EventForm, self).__init__(*args, **kwargs) + self.fields['destination'].widget.can_delete_related = False + self.fields['destination'].widget.can_add_related = False + self.fields['destination'].widget.can_change_related = False + def clean(self): """Validate start date is less than end date""" cleaned_data = super(EventForm, self).clean() From 6ee91ca77d03829650f8749d3e2ce8b8faaaa1d4 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Wed, 29 Nov 2017 12:02:52 -0500 Subject: [PATCH 17/31] Remove FeedEvent The Uwishunu GeoRSS event feed has gone away and is not coming back. No suitable event feeds with location data have been found to replace it. Remove the no longer used model, cron job, and related views, to prevent confusion with the new, internal Event type. Also remove unused print destinations view. --- .../cac-tripplanner.app/defaults/main.yml | 3 - .../roles/cac-tripplanner.app/tasks/main.yml | 11 -- .../cac_tripplanner/cac_tripplanner/urls.py | 4 - .../management/commands/load_events.py | 123 ------------------ .../migrations/0022_delete_feedevent.py | 18 +++ python/cac_tripplanner/destinations/models.py | 39 ------ python/cac_tripplanner/destinations/tests.py | 51 +------- python/cac_tripplanner/destinations/views.py | 40 +----- 8 files changed, 20 insertions(+), 269 deletions(-) delete mode 100644 python/cac_tripplanner/destinations/management/commands/load_events.py create mode 100644 python/cac_tripplanner/destinations/migrations/0022_delete_feedevent.py diff --git a/deployment/ansible/roles/cac-tripplanner.app/defaults/main.yml b/deployment/ansible/roles/cac-tripplanner.app/defaults/main.yml index f55b55de6..804ba4bfc 100644 --- a/deployment/ansible/roles/cac-tripplanner.app/defaults/main.yml +++ b/deployment/ansible/roles/cac-tripplanner.app/defaults/main.yml @@ -9,7 +9,6 @@ production: False app_sass_version: "3.4.22" app_log: "/var/log/cac-tripplanner-app.log" -app_cron_event_feed_log: "/var/log/event-feed.log" root_app_dir: "/opt/app/python/cac_tripplanner" root_conf_dir: "/etc/cac_tripplanner.d" @@ -17,8 +16,6 @@ root_src_dir: "/opt/app/src" root_static_dir: "/srv/cac" root_media_dir: "/media/cac" -app_cron_event_feed: "cd {{ root_app_dir }} && python manage.py load_events >> {{ app_cron_event_feed_log }} 2>&1" - cac_python_dependencies: - { name: 'base58', version: '0.2.5' } - { name: 'boto', version: '2.48.0' } diff --git a/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml b/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml index db90cb92b..bcd8b39b6 100644 --- a/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml +++ b/deployment/ansible/roles/cac-tripplanner.app/tasks/main.yml @@ -78,17 +78,6 @@ template: src=nginx-default.j2 dest=/etc/nginx/sites-available/default notify: Restart nginx -- name: Touch cron job log file if it does not exist - copy: content="" dest="{{ app_cron_event_feed_log }}" force=no - -- name: Touch cron job log file if it does not exist, and set permissions - file: path={{ app_cron_event_feed_log}} state=touch - owner={{ app_username }} group={{ app_username }} mode=0664 - -# TODO: Add logic in production to only run on a single webserver -- name: Add cron job for RSS events feed - cron: name="Load event feed" minute="45" user="{{ app_username }}" job="{{ app_cron_event_feed }}" - - { include: jslibs.yml } - { include: dev-test-dependencies.yml, when: develop or test } diff --git a/python/cac_tripplanner/cac_tripplanner/urls.py b/python/cac_tripplanner/cac_tripplanner/urls.py index 74e2367a8..b5cf3dd43 100644 --- a/python/cac_tripplanner/cac_tripplanner/urls.py +++ b/python/cac_tripplanner/cac_tripplanner/urls.py @@ -21,12 +21,8 @@ # Map url(r'^api/destinations/search$', dest_views.SearchDestinations.as_view(), name='api_destinations_search'), - url(r'^api/feedevents$', dest_views.FeedEvents.as_view(), name='api_feedevents'), url(r'^map/reachable$', dest_views.FindReachableDestinations.as_view(), name='reachable'), - # print directions view. TODO: update or delete - # url(r'^directions/', dest_views.directions, name='directions'), - # Handle pre-redesign URLs by redirecting url(r'^(?:map/)?directions/', RedirectView.as_view(pattern_name='home', query_string=True, permanent=True)), diff --git a/python/cac_tripplanner/destinations/management/commands/load_events.py b/python/cac_tripplanner/destinations/management/commands/load_events.py deleted file mode 100644 index 072dab696..000000000 --- a/python/cac_tripplanner/destinations/management/commands/load_events.py +++ /dev/null @@ -1,123 +0,0 @@ -from datetime import datetime -import urllib2 -from xml.dom import minidom - -from django.core.management.base import BaseCommand -from django.contrib.gis.geos import Point -from destinations.models import FeedEvent - -from pytz import utc - - -class Command(BaseCommand): - args = '' - help = 'Load the current Uwishunu feed into the FeedEvent table' - - now = utc.localize(datetime.utcnow()) - - def property_exists(self, item, property_name): - """ Check if a property exists as a child of the given element """ - return item.getElementsByTagName(property_name).length > 0 - - def get_property(self, item, property_name): - """ Retrieve the value of an xml property name from another xml element """ - elements = item.getElementsByTagName(property_name) - if elements.length > 0 and elements[0].firstChild: - return elements[0].firstChild.data - else: - return None - - def parse_date(self, string_datetime): - """ Parse a date from the feed, return datetime localized to US/Eastern """ - if not string_datetime: - return None - try: - parsed_date = datetime.strptime(string_datetime, '%a, %d %b %Y %H:%M:%S +0000') - except ValueError: - return None - return utc.localize(parsed_date) - - def handle(self, *args, **options): - """ Retrieve and populate FeedEvent table from an RSS Feed """ - - url = 'http://www.uwishunu.com/feed/google/' - # Get 403 forbidden without changing user-agent - headers = { - 'User-Agent': 'Mozilla/5.0 (X11; Linux i686; rv:10.0) Gecko/20100101 Firefox/10.0' - } - request = urllib2.Request(url, None, headers) - feed = minidom.parse(urllib2.urlopen(request)) - - for item in feed.getElementsByTagName('item'): - self.handle_feed_item(item) - - self.delete_expired() - - def handle_feed_item(self, item): - """ Update or create a FeedEvent based on a unique identifier """ - - # Unique field - guid = self.get_property(item, 'guid') - - # need a lat/lng to care about this item - if not self.property_exists(item, 'georss:point'): - return - - # Other fields - author = self.get_property(item, 'dc:creator') - - categories_list = [cat.firstChild.data for cat in item.getElementsByTagName('category')] - categories = ','.join(categories_list) - - content = self.get_property(item, 'content:encoded') - - description = self.get_property(item, 'description') - - link = self.get_property(item, 'link') - - try: - georss = self.get_property(item, 'georss:point').split() - lat = float(georss[0]) - lon = float(georss[1]) - except (ValueError, IndexError): - self.stdout.write('Unable to convert lat/lng for: {0}'.format(guid)) - return - - point = Point(lon, lat) - - publication_date = self.parse_date(self.get_property(item, 'pubDate')) - end_date = self.parse_date(self.get_property(item, 'fieldtrip:endDate')) - if publication_date is None or end_date is None or end_date < self.now: - # Skip event if in past or bad/empty dates - return - - image_url = self.get_property(item, 'url') - - title = self.get_property(item, 'title') - - # Update or create the FeedEvent - updated_item = { - 'author': author, - 'categories': categories, - 'content': content, - 'description': description, - 'link': link, - 'point': point, - 'publication_date': publication_date, - 'end_date': end_date, - 'image_url': image_url, - 'title': title, - } - - feed_event, created = FeedEvent.objects.update_or_create(guid=guid, defaults=updated_item) - if created: - self.stdout.write('{0}: Added event: "{1}"'.format(str(datetime.utcnow()), guid)) - - def delete_expired(self): - """ Clear events with end_date < now """ - expired_events = FeedEvent.objects.filter(end_date__lt=self.now) - num_expired_events = len(expired_events) - if num_expired_events > 0: - expired_events.delete() - self.stdout.write('{0}: Cleaned {1} expired events'.format(str(datetime.now()), - num_expired_events)) diff --git a/python/cac_tripplanner/destinations/migrations/0022_delete_feedevent.py b/python/cac_tripplanner/destinations/migrations/0022_delete_feedevent.py new file mode 100644 index 000000000..d7952a3f2 --- /dev/null +++ b/python/cac_tripplanner/destinations/migrations/0022_delete_feedevent.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.7 on 2017-11-29 16:59 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('destinations', '0021_event'), + ] + + operations = [ + migrations.DeleteModel( + name='FeedEvent', + ), + ] diff --git a/python/cac_tripplanner/destinations/models.py b/python/cac_tripplanner/destinations/models.py index da1e67aff..67c97d65b 100644 --- a/python/cac_tripplanner/destinations/models.py +++ b/python/cac_tripplanner/destinations/models.py @@ -82,42 +82,3 @@ class Meta: def __unicode__(self): return self.name - - -class FeedEventManager(models.GeoManager): - """Custom manager for FeedEvents allows filtering on publication_date""" - - def published(self): - return self.get_queryset().filter(publication_date__lt=now()).filter(end_date__gt=now()) - - def get_queryset(self): - return super(FeedEventManager, self).get_queryset() - - -class FeedEvent(models.Model): - """ Model for RSS Feed Events, currently served by Uwishunu """ - - guid = models.CharField(unique=True, max_length=64) - title = models.CharField(max_length=512, null=True) - link = models.CharField(max_length=512, null=True) - image_url = models.URLField(blank=True, null=True) - author = models.CharField(max_length=64, null=True) - publication_date = models.DateTimeField() - end_date = models.DateTimeField(default=now) - categories = models.CharField(max_length=512, null=True) - description = models.CharField(max_length=512, null=True) - content = RichTextField(blank=True, null=True) - point = models.PointField() - - @property - def published(self): - """Helper property to easily determine if an article is published""" - if self.publication_date and self.end_date: - return self.publication_date < now() and self.end_date > now() - else: - return False - - def __unicode__(self): - return self.title - - objects = FeedEventManager() diff --git a/python/cac_tripplanner/destinations/tests.py b/python/cac_tripplanner/destinations/tests.py index 81469b867..be118ee3c 100644 --- a/python/cac_tripplanner/destinations/tests.py +++ b/python/cac_tripplanner/destinations/tests.py @@ -6,7 +6,7 @@ from django.test import Client, TestCase from django.utils.timezone import now -from destinations.models import Destination, Event, FeedEvent +from destinations.models import Destination, Event class EventTests(TestCase): @@ -91,52 +91,3 @@ def test_place_detail_view(self): kwargs={'pk': self.place_3.pk}) response_404 = self.client.get(url) self.assertEqual(response_404.status_code, 404) - - -class FeedEventTests(TestCase): - - def setUp(self): - common_args = { - 'point': Point(0, 0), - 'title': 'Test article', - 'content': 'Test content', - 'description': 'Test description', - 'link': 'http://uwishunu.com', - 'author': 'John Smith', - 'categories': 'Events' - } - past_published = now() - timedelta(hours=1) - future_published = now() + timedelta(hours=1) - - self.pub_future_end_past = FeedEvent.objects.create( - guid='1', - publication_date=future_published, - end_date=past_published, - **common_args) - self.pub_past_end_future = FeedEvent.objects.create( - guid='2', - publication_date=past_published, - end_date=future_published, - **common_args) - self.pub_future_end_future = FeedEvent.objects.create( - guid='3', - publication_date=future_published, - end_date=future_published, - **common_args) - self.pub_past_end_past = FeedEvent.objects.create( - guid='4', - publication_date=past_published, - end_date=past_published, - **common_args) - - def test_feed_event_manager(self): - - published_count = FeedEvent.objects.published().count() - self.assertEqual(published_count, 1) - - def test_published_property(self): - """ Only events that have published < now and end_date > now should be valid """ - self.assertFalse(self.pub_future_end_past.published) - self.assertTrue(self.pub_past_end_future.published) - self.assertFalse(self.pub_future_end_future.published) - self.assertFalse(self.pub_past_end_past.published) diff --git a/python/cac_tripplanner/destinations/views.py b/python/cac_tripplanner/destinations/views.py index 0c7e26519..7ea6ebdff 100644 --- a/python/cac_tripplanner/destinations/views.py +++ b/python/cac_tripplanner/destinations/views.py @@ -10,7 +10,7 @@ from django.shortcuts import get_object_or_404, render from django.views.generic import View -from .models import Destination, FeedEvent +from .models import Destination from cms.models import Article @@ -66,16 +66,6 @@ def explore(request): return base_view(request, 'home.html', context=context) -def directions(request): - """ - The directions view - - :param request: Request object - :returns: A rendered response - """ - return base_view(request, 'directions.html', {}) - - def manifest(request): """Render the app manifest for a PWA app that can install to homescreen @@ -269,31 +259,3 @@ def get(self, request, *args, **kwargs): response = {'destinations': data} return HttpResponse(json.dumps(response), 'application/json') - - -class FeedEvents(View): - """ API endpoint for the FeedEvent model """ - - def get(self, request, *args, **kwargs): - """ GET 20 most recent feed events that are published - - TODO: Additional filtering - """ - utc = timezone('UTC') - epoch = utc.localize(datetime(1970, 1, 1)) - - try: - limit = int(request.GET.get('limit')) - except (ValueError, TypeError): - limit = settings.HOMEPAGE_RESULTS_LIMIT - - results = FeedEvent.objects.published().order_by('end_date')[:limit] - response = [model_to_dict(x) for x in results] - for obj in response: - pnt = obj['point'] - obj['point'] = json.loads(pnt.json) - dt = obj['publication_date'] - obj['publication_date'] = (dt - epoch).total_seconds() - dt = obj['end_date'] - obj['end_date'] = (dt - epoch).total_seconds() - return HttpResponse(json.dumps(response), 'application/json') From 37966611ff6a37e4d14d4151510947ef42de5148 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Wed, 29 Nov 2017 11:48:10 -0500 Subject: [PATCH 18/31] Add destination categories Add model for destination categories. Add as nullable foreign key to destination. Closes #844. --- .../migrations/0023_auto_20171129_1511.py | 31 +++++++++++++++++++ python/cac_tripplanner/destinations/models.py | 13 ++++++++ 2 files changed, 44 insertions(+) create mode 100644 python/cac_tripplanner/destinations/migrations/0023_auto_20171129_1511.py diff --git a/python/cac_tripplanner/destinations/migrations/0023_auto_20171129_1511.py b/python/cac_tripplanner/destinations/migrations/0023_auto_20171129_1511.py new file mode 100644 index 000000000..930035718 --- /dev/null +++ b/python/cac_tripplanner/destinations/migrations/0023_auto_20171129_1511.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.7 on 2017-11-29 20:11 +from __future__ import unicode_literals + +import ckeditor.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('destinations', '0022_delete_feedevent'), + ] + + operations = [ + migrations.CreateModel( + name='DestinationCategory', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, unique=True)), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.AddField( + model_name='destination', + name='categories', + field=models.ManyToManyField(to='destinations.DestinationCategory'), + ), + ] diff --git a/python/cac_tripplanner/destinations/models.py b/python/cac_tripplanner/destinations/models.py index 67c97d65b..b2b3d2690 100644 --- a/python/cac_tripplanner/destinations/models.py +++ b/python/cac_tripplanner/destinations/models.py @@ -27,6 +27,18 @@ def upcoming(self): return self.get_queryset().filter(start_date__gt=now()) +class DestinationCategory(models.Model): + """Categories for filtering destinations""" + + class Meta: + ordering = ['name', ] + + name = models.CharField(max_length=50, unique=True) + + def __unicode__(self): + return self.name + + class Destination(models.Model): """Represents a destination""" @@ -51,6 +63,7 @@ class Meta: help_text='The large image. Will be displayed at 680x400.') published = models.BooleanField(default=False) priority = models.IntegerField(default=9999, null=False) + categories = models.ManyToManyField('DestinationCategory') objects = DestinationManager() From 3a245f9aaf71b23a7b7461362ccd62102635dc8e Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Wed, 29 Nov 2017 15:40:12 -0500 Subject: [PATCH 19/31] Add default categories Create default destination categories and set them on the default destinations via data migration. --- .../0024_default_destination_categories.py | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 python/cac_tripplanner/destinations/migrations/0024_default_destination_categories.py diff --git a/python/cac_tripplanner/destinations/migrations/0024_default_destination_categories.py b/python/cac_tripplanner/destinations/migrations/0024_default_destination_categories.py new file mode 100644 index 000000000..88919857d --- /dev/null +++ b/python/cac_tripplanner/destinations/migrations/0024_default_destination_categories.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + + +def get_sample_categories(): + return [ + { + 'name': 'Nature', + }, + { + 'name': 'Exercise', + }, + { + 'name': 'Relax', + }, + { + 'name': 'Educational', + }, + ] + +def get_sample_destinations(): + """Names of default destinations added in migration 0012, with default categories to add""" + return [ + { + 'name': 'Fairmount Waterworks Interpretive Center', + 'categories': ('Nature', 'Educational',) + }, + { + 'name': 'Independence Seaport Museum', + 'categories': ('Educational',) + }, + { + 'name': 'Bartram\'s Garden', + 'categories': ('Nature', 'Relax', 'Exercise') + }, + { + 'name': 'Schuylkill Environmental Education Center', + 'categories': ('Nature', 'Educational',) + }, + { + 'name': 'John Heinz National Wildlife Refuge', + 'categories': ('Nature', 'Relax',) + }, + { + 'name': 'NJ Academy of Aquatic Sciences', + 'categories': ('Educational',) + }, + { + 'name': 'Schuylkill River Greenway Association', + 'categories': ('Nature', 'Exercise',) + }, + { + 'name': 'Palmyra Cove Nature Park', + 'categories': ('Nature', 'Exercise',) + }, + { + 'name': 'Tulpehaking Nature Center at Abbott Marshland', + 'categories': ('Nature', 'Educational',) + }, + ] + +def add_sample_categories(apps, schema_editor): + DestinationCategory = apps.get_model('destinations', 'DestinationCategory') + Destination = apps.get_model('destinations', 'Destination') + # If categories already exist, do nothing + if DestinationCategory.objects.count() > 0: + return + + for category in get_sample_categories(): + sample_categories = DestinationCategory.objects.filter(name=category['name']) + if len(sample_categories) == 0: + sample_dest = DestinationCategory(**category) + sample_dest.save() + + # set the new categories on the default destinations that were added in migration 0012 + for dest in get_sample_destinations(): + destination = Destination.objects.get(name=dest['name']) + if not destination: + continue + for add_category in dest['categories']: + category = DestinationCategory.objects.get(name=add_category) + if category: + destination.categories.add(category) + destination.save() + + +def delete_sample_categories(apps, schema_editor): + DestinationCategory = apps.get_model('destinations', 'DestinationCategory') + for category in get_sample_categories(): + try: + sample_categories = DestinationCategory.objects.filter(name=category['name']) + sample_categories.delete() + except DestinationCategory.DoesNotExist: + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('destinations', '0023_auto_20171129_1511'), + ] + + operations = [ + migrations.RunPython(add_sample_categories, delete_sample_categories), + ] From e15c30e7c7653950510990a3a00ff753fac70650 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Wed, 29 Nov 2017 17:45:17 -0500 Subject: [PATCH 20/31] Serialize destination categories Send category name when serializing destinations to JSON. --- python/cac_tripplanner/destinations/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/cac_tripplanner/destinations/views.py b/python/cac_tripplanner/destinations/views.py index 7ea6ebdff..c9d392c3d 100644 --- a/python/cac_tripplanner/destinations/views.py +++ b/python/cac_tripplanner/destinations/views.py @@ -144,6 +144,7 @@ def set_destination_properties(destination): 'Region': obj['state'], 'StAddr': obj['address'] } + obj['categories'] = [c.name for c in obj['categories']] return obj From 74db73f0a84784bf77e517ce2b667bedadcc15ae Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Thu, 30 Nov 2017 10:49:38 -0500 Subject: [PATCH 21/31] Retain flake8 exit status --- scripts/lint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/lint.sh b/scripts/lint.sh index fd2f60129..226cb4573 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -12,7 +12,7 @@ trap 'mark_unstable' ERR vagrant ssh app -c "touch /opt/app/python/violations.txt" # get console output and to write to file vagrant ssh app -c "flake8 /opt/app/python --exclude=migrations \ - --output-file=/opt/app/python/violations.txt --exit-zero --tee" + --output-file=/opt/app/python/violations.txt --tee" # Run JS linting vagrant ssh app -c "cd /opt/app/src && npm run gulp-lint" From a3352021ab56ad4042a6707014ed8cc760e8df0b Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Thu, 30 Nov 2017 10:50:13 -0500 Subject: [PATCH 22/31] Remove unused imports --- python/cac_tripplanner/destinations/forms.py | 1 - python/cac_tripplanner/destinations/views.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/python/cac_tripplanner/destinations/forms.py b/python/cac_tripplanner/destinations/forms.py index 3428d3dc3..481a3e21b 100644 --- a/python/cac_tripplanner/destinations/forms.py +++ b/python/cac_tripplanner/destinations/forms.py @@ -1,4 +1,3 @@ -from django.contrib.admin import widgets from django.forms import ModelForm, ValidationError from .models import Destination, Event diff --git a/python/cac_tripplanner/destinations/views.py b/python/cac_tripplanner/destinations/views.py index c9d392c3d..a56b45884 100644 --- a/python/cac_tripplanner/destinations/views.py +++ b/python/cac_tripplanner/destinations/views.py @@ -1,5 +1,3 @@ -from datetime import datetime -from pytz import timezone import json import requests From 21cd5f379f1ddfe3d8bfd59dc85a901da17fa2fd Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Thu, 30 Nov 2017 11:56:15 -0500 Subject: [PATCH 23/31] Filter destinations by category Add `categories` parameter to destinations search endpoint to filter by category. Takes a comma-separated list. --- python/cac_tripplanner/destinations/views.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/python/cac_tripplanner/destinations/views.py b/python/cac_tripplanner/destinations/views.py index a56b45884..ef20d84fa 100644 --- a/python/cac_tripplanner/destinations/views.py +++ b/python/cac_tripplanner/destinations/views.py @@ -216,7 +216,8 @@ def get(self, request, *args, **kwargs): - lat + lon params - text param Optional: - - limit param + - limit param: maximum number of results to return (integer) + - categories param: comma-separated list of destination category names to filter to A search via text will return destinations that match the destination name A search via lat/lon will return destinations that are closest to the search point @@ -227,6 +228,7 @@ def get(self, request, *args, **kwargs): lon = params.get('lon', None) text = params.get('text', None) limit = params.get('limit', None) + categories = params.get('categories', None) results = [] if lat and lon: @@ -243,6 +245,10 @@ def get(self, request, *args, **kwargs): .order_by('distance', 'priority')) elif text is not None: results = Destination.objects.filter(published=True, name__icontains=text) + + if categories: + results = results.filter(categories__name__in=categories.split(',')) + if limit: try: limit_int = int(limit) From 7f175e7459579708d5154d7bc0111535c6359fd4 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Thu, 30 Nov 2017 11:59:12 -0500 Subject: [PATCH 24/31] Filter reachable destinations by category Add `categories` parameter to reachable destinations endpoint to allow filtering by comma-separated list of destination category names. Closes #846. --- python/cac_tripplanner/destinations/views.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/python/cac_tripplanner/destinations/views.py b/python/cac_tripplanner/destinations/views.py index ef20d84fa..34e7c1861 100644 --- a/python/cac_tripplanner/destinations/views.py +++ b/python/cac_tripplanner/destinations/views.py @@ -176,7 +176,9 @@ def isochrone(self, payload): def get(self, request, *args, **kwargs): """When a GET hits this endpoint, calculate an isochrone and find destinations within it. - Return both the isochrone GeoJSON and the list of matched destinations.""" + Return both the isochrone GeoJSON and the list of matched destinations. + + Can send optional comma-separated `categories` param to filter by destination category.""" params = request.GET.copy() # make mutable # allow a max travelshed size of 60 minutes in a query @@ -199,6 +201,10 @@ def get(self, request, *args, **kwargs): else: matched_objects = [] + categories = params.get('categories', None) + if categories: + matched_objects = matched_objects.filter(categories__name__in=categories.split(',')) + # make locations JSON serializable matched_objects = [set_destination_properties(x) for x in matched_objects] From 641a96b60f869d91c91a2bf922af691203d3aa02 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Fri, 1 Dec 2017 12:11:45 -0500 Subject: [PATCH 25/31] Add accessibility flag Add `accessible` flag to destinations and events, to note if they are ADA accessible or not. Closes #918. --- .../migrations/0025_auto_20171201_1209.py | 25 +++++++++++++++++++ python/cac_tripplanner/destinations/models.py | 2 ++ 2 files changed, 27 insertions(+) create mode 100644 python/cac_tripplanner/destinations/migrations/0025_auto_20171201_1209.py diff --git a/python/cac_tripplanner/destinations/migrations/0025_auto_20171201_1209.py b/python/cac_tripplanner/destinations/migrations/0025_auto_20171201_1209.py new file mode 100644 index 000000000..eca05c2d3 --- /dev/null +++ b/python/cac_tripplanner/destinations/migrations/0025_auto_20171201_1209.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.7 on 2017-12-01 17:09 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('destinations', '0024_default_destination_categories'), + ] + + operations = [ + migrations.AddField( + model_name='destination', + name='accessible', + field=models.BooleanField(default=False, help_text=b'Is it ADA accessible?'), + ), + migrations.AddField( + model_name='event', + name='accessible', + field=models.BooleanField(default=False, help_text=b'Is it ADA accessible?'), + ), + ] diff --git a/python/cac_tripplanner/destinations/models.py b/python/cac_tripplanner/destinations/models.py index b2b3d2690..1dcafdb3c 100644 --- a/python/cac_tripplanner/destinations/models.py +++ b/python/cac_tripplanner/destinations/models.py @@ -64,6 +64,7 @@ class Meta: published = models.BooleanField(default=False) priority = models.IntegerField(default=9999, null=False) categories = models.ManyToManyField('DestinationCategory') + accessible = models.BooleanField(default=False, help_text='Is it ADA accessible?') objects = DestinationManager() @@ -90,6 +91,7 @@ class Meta: help_text='The large image. Will be displayed at 680x400.') published = models.BooleanField(default=False) priority = models.IntegerField(default=9999, null=False) + accessible = models.BooleanField(default=False, help_text='Is it ADA accessible?') objects = EventManager() From b6493f2d9e4454f5807454dd9922411364df880b Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Fri, 1 Dec 2017 14:02:53 -0500 Subject: [PATCH 26/31] Move shared properties to Attractions Create new abstract base class for shared properties of destinations and events. --- python/cac_tripplanner/destinations/models.py | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/python/cac_tripplanner/destinations/models.py b/python/cac_tripplanner/destinations/models.py index 1dcafdb3c..a94b5d199 100644 --- a/python/cac_tripplanner/destinations/models.py +++ b/python/cac_tripplanner/destinations/models.py @@ -39,15 +39,30 @@ def __unicode__(self): return self.name -class Destination(models.Model): - """Represents a destination""" +class Attraction(models.Model): + """Shared properties of destinations and events""" class Meta: - ordering = ['priority', '?'] + abstract = True name = models.CharField(max_length=50) website_url = models.URLField(blank=True, null=True) description = RichTextField() + image = models.ImageField(upload_to=generate_filename, null=True, + help_text='The small image. Will be displayed at 310x155.') + wide_image = models.ImageField(upload_to=generate_filename, null=True, + help_text='The large image. Will be displayed at 680x400.') + published = models.BooleanField(default=False) + priority = models.IntegerField(default=9999, null=False) + accessible = models.BooleanField(default=False, help_text='Is it ADA accessible?') + + +class Destination(Attraction): + """Represents a destination""" + + class Meta: + ordering = ['priority', '?'] + city = models.CharField(max_length=40, default='Philadelphia') state = models.CharField(max_length=20, default='PA') zip = models.CharField(max_length=5, null=True) @@ -57,42 +72,24 @@ class Meta: help_text=('The map automatically updates as the address is typed, ' 'but may be overridden manually if incorrect.')) point = models.PointField() - image = models.ImageField(upload_to=generate_filename, null=True, - help_text='The small image. Will be displayed at 310x155.') - wide_image = models.ImageField(upload_to=generate_filename, null=True, - help_text='The large image. Will be displayed at 680x400.') - published = models.BooleanField(default=False) - priority = models.IntegerField(default=9999, null=False) categories = models.ManyToManyField('DestinationCategory') - accessible = models.BooleanField(default=False, help_text='Is it ADA accessible?') objects = DestinationManager() def __unicode__(self): return self.name -class Event(models.Model): +class Event(Attraction): """Represents an event, which has a start and end date""" class Meta: ordering = ['priority', '-start_date'] - name = models.CharField(max_length=50) - website_url = models.URLField(blank=True, null=True) - description = RichTextField() start_date = models.DateTimeField() end_date = models.DateTimeField() destination = models.ForeignKey('Destination', on_delete=models.SET_NULL, null=True, blank=True) - image = models.ImageField(upload_to=generate_filename, null=True, - help_text='The small image. Will be displayed at 310x155.') - wide_image = models.ImageField(upload_to=generate_filename, null=True, - help_text='The large image. Will be displayed at 680x400.') - published = models.BooleanField(default=False) - priority = models.IntegerField(default=9999, null=False) - accessible = models.BooleanField(default=False, help_text='Is it ADA accessible?') - objects = EventManager() def __unicode__(self): From bd7c5b2c1580465edd63427b87cde139dee5a813 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Fri, 1 Dec 2017 14:30:34 -0500 Subject: [PATCH 27/31] Rename zip to zipcode Rename destinations zip code field to avoid conflict with built-in function. No front end changes needed, as zip code there referenced using `Postal` attribute. Closes #919. --- .../cac_tripplanner/cac_tripplanner/tests.py | 4 ++-- python/cac_tripplanner/destinations/admin.py | 2 +- .../migrations/0026_auto_20171201_1427.py | 20 +++++++++++++++++++ python/cac_tripplanner/destinations/models.py | 2 +- python/cac_tripplanner/destinations/views.py | 2 +- 5 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 python/cac_tripplanner/destinations/migrations/0026_auto_20171201_1427.py diff --git a/python/cac_tripplanner/cac_tripplanner/tests.py b/python/cac_tripplanner/cac_tripplanner/tests.py index 6378e9b81..ee4bf69ee 100644 --- a/python/cac_tripplanner/cac_tripplanner/tests.py +++ b/python/cac_tripplanner/cac_tripplanner/tests.py @@ -30,7 +30,7 @@ def setUp(self): address='123 Test ln.', city='Gotham', state='Euphoria', - zip='12345', + zipcode='12345', published=True ) Destination.objects.create( @@ -40,7 +40,7 @@ def setUp(self): address='123 Test ln.', city='Thangorodrim', state='Angband', - zip='12349', + zipcode='12349', published=True ) diff --git a/python/cac_tripplanner/destinations/admin.py b/python/cac_tripplanner/destinations/admin.py index 52078587e..7723496e7 100644 --- a/python/cac_tripplanner/destinations/admin.py +++ b/python/cac_tripplanner/destinations/admin.py @@ -8,7 +8,7 @@ class DestinationAdmin(gis.admin.OSMGeoAdmin): form = DestinationForm - list_display = ('name', 'published', 'priority', 'address', 'city', 'state', 'zip') + list_display = ('name', 'published', 'priority', 'address', 'city', 'state', 'zipcode') actions = ('make_published', 'make_unpublished') ordering = ('name', ) diff --git a/python/cac_tripplanner/destinations/migrations/0026_auto_20171201_1427.py b/python/cac_tripplanner/destinations/migrations/0026_auto_20171201_1427.py new file mode 100644 index 000000000..beb758e5c --- /dev/null +++ b/python/cac_tripplanner/destinations/migrations/0026_auto_20171201_1427.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.7 on 2017-12-01 19:27 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('destinations', '0025_auto_20171201_1209'), + ] + + operations = [ + migrations.RenameField( + model_name='destination', + old_name='zip', + new_name='zipcode', + ), + ] diff --git a/python/cac_tripplanner/destinations/models.py b/python/cac_tripplanner/destinations/models.py index a94b5d199..249011633 100644 --- a/python/cac_tripplanner/destinations/models.py +++ b/python/cac_tripplanner/destinations/models.py @@ -65,7 +65,7 @@ class Meta: city = models.CharField(max_length=40, default='Philadelphia') state = models.CharField(max_length=20, default='PA') - zip = models.CharField(max_length=5, null=True) + zipcode = models.CharField(max_length=5, null=True) # In the admin interface, display the address right above the map, since it triggers geocoding address = models.CharField(max_length=40, null=True, diff --git a/python/cac_tripplanner/destinations/views.py b/python/cac_tripplanner/destinations/views.py index 34e7c1861..cd9b2e510 100644 --- a/python/cac_tripplanner/destinations/views.py +++ b/python/cac_tripplanner/destinations/views.py @@ -138,7 +138,7 @@ def set_destination_properties(destination): obj['location'] = {'x': x, 'y': y} obj['attributes'] = { 'City': obj['city'], - 'Postal': obj['zip'], + 'Postal': obj['zipcode'], 'Region': obj['state'], 'StAddr': obj['address'] } From c99e63bbeb78d760de3c47a6a7acb72eb8817ec8 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Fri, 1 Dec 2017 14:40:54 -0500 Subject: [PATCH 28/31] Add activities Add model for things to do at a destination or an event. Closes #917. --- .../migrations/0027_auto_20171201_1439.py | 35 +++++++++++++++++++ python/cac_tripplanner/destinations/models.py | 13 +++++++ 2 files changed, 48 insertions(+) create mode 100644 python/cac_tripplanner/destinations/migrations/0027_auto_20171201_1439.py diff --git a/python/cac_tripplanner/destinations/migrations/0027_auto_20171201_1439.py b/python/cac_tripplanner/destinations/migrations/0027_auto_20171201_1439.py new file mode 100644 index 000000000..2e9ab3b45 --- /dev/null +++ b/python/cac_tripplanner/destinations/migrations/0027_auto_20171201_1439.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.7 on 2017-12-01 19:39 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('destinations', '0026_auto_20171201_1427'), + ] + + operations = [ + migrations.CreateModel( + name='Activity', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, unique=True)), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.AddField( + model_name='destination', + name='activities', + field=models.ManyToManyField(to='destinations.Activity'), + ), + migrations.AddField( + model_name='event', + name='activities', + field=models.ManyToManyField(to='destinations.Activity'), + ), + ] diff --git a/python/cac_tripplanner/destinations/models.py b/python/cac_tripplanner/destinations/models.py index 249011633..1c327d84a 100644 --- a/python/cac_tripplanner/destinations/models.py +++ b/python/cac_tripplanner/destinations/models.py @@ -39,6 +39,18 @@ def __unicode__(self): return self.name +class Activity(models.Model): + """Possible things to do at an Attraction""" + + class Meta: + ordering = ['name', ] + + name = models.CharField(max_length=50, unique=True) + + def __unicode__(self): + return self.name + + class Attraction(models.Model): """Shared properties of destinations and events""" @@ -55,6 +67,7 @@ class Meta: published = models.BooleanField(default=False) priority = models.IntegerField(default=9999, null=False) accessible = models.BooleanField(default=False, help_text='Is it ADA accessible?') + activities = models.ManyToManyField('Activity') class Destination(Attraction): From a97fdfcab9817741ff417c9e19de94727a9de85a Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Fri, 1 Dec 2017 15:19:46 -0500 Subject: [PATCH 29/31] Add default activities Add default activities and set them on the default destinations. --- .../0024_default_destination_categories.py | 1 - .../migrations/0028_default_activities.py | 103 ++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 python/cac_tripplanner/destinations/migrations/0028_default_activities.py diff --git a/python/cac_tripplanner/destinations/migrations/0024_default_destination_categories.py b/python/cac_tripplanner/destinations/migrations/0024_default_destination_categories.py index 88919857d..38de90b44 100644 --- a/python/cac_tripplanner/destinations/migrations/0024_default_destination_categories.py +++ b/python/cac_tripplanner/destinations/migrations/0024_default_destination_categories.py @@ -83,7 +83,6 @@ def add_sample_categories(apps, schema_editor): category = DestinationCategory.objects.get(name=add_category) if category: destination.categories.add(category) - destination.save() def delete_sample_categories(apps, schema_editor): diff --git a/python/cac_tripplanner/destinations/migrations/0028_default_activities.py b/python/cac_tripplanner/destinations/migrations/0028_default_activities.py new file mode 100644 index 000000000..62db29cc7 --- /dev/null +++ b/python/cac_tripplanner/destinations/migrations/0028_default_activities.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + + +def get_sample_activities(): + return [ + { + 'name': 'cycling', + }, + { + 'name': 'hiking', + }, + { + 'name': 'canoeing', + }, + ] + +def get_sample_destinations(): + """Names of default destinations added in migration 0012, with default activities to add""" + return [ + { + 'name': 'Fairmount Waterworks Interpretive Center', + 'activities': () + }, + { + 'name': 'Independence Seaport Museum', + 'activities': () + }, + { + 'name': 'Bartram\'s Garden', + 'activities': ('cycling', 'hiking',) + }, + { + 'name': 'Schuylkill Environmental Education Center', + 'activities': ('canoeing',) + }, + { + 'name': 'John Heinz National Wildlife Refuge', + 'activities': ('hiking',) + }, + { + 'name': 'NJ Academy of Aquatic Sciences', + 'activities': () + }, + { + 'name': 'Schuylkill River Greenway Association', + 'activities': () + }, + { + 'name': 'Palmyra Cove Nature Park', + 'activities': ('hiking',) + }, + { + 'name': 'Tulpehaking Nature Center at Abbott Marshland', + 'activities': () + }, + ] + +def add_sample_activities(apps, schema_editor): + Activity = apps.get_model('destinations', 'Activity') + Destination = apps.get_model('destinations', 'Destination') + # If activities already exist, do nothing + if Activity.objects.count() > 0: + return + + for activity in get_sample_activities(): + sample_activities = Activity.objects.filter(name=activity['name']) + if len(sample_activities) == 0: + sample_dest = Activity(**activity) + sample_dest.save() + + # set the new activities on the default destinations that were added in migration 0012 + for dest in get_sample_destinations(): + destination = Destination.objects.get(name=dest['name']) + if not destination: + continue + for add_activity in dest['activities']: + activity = Activity.objects.get(name=add_activity) + if activity: + destination.activities.add(activity) + + +def delete_sample_activities(apps, schema_editor): + Activity = apps.get_model('destinations', 'Activity') + for activity in get_sample_activities(): + try: + sample_activities = Activity.objects.filter(name=activity['name']) + sample_activities.delete() + except Activity.DoesNotExist: + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('destinations', '0027_auto_20171201_1439'), + ] + + operations = [ + migrations.RunPython(add_sample_activities, delete_sample_activities), + ] From 24d127d23a8b2d3aa49ce91dad0b9a999202bc8b Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Fri, 1 Dec 2017 15:36:22 -0500 Subject: [PATCH 30/31] Add watershed alliance flag Add flag to indicate if a destination belongs to the Watershed Alliance. Closes #926. --- .../0029_destination_watershed_alliance.py | 20 +++++++++ .../0030_default_watershed_alliance.py | 44 +++++++++++++++++++ python/cac_tripplanner/destinations/models.py | 4 ++ 3 files changed, 68 insertions(+) create mode 100644 python/cac_tripplanner/destinations/migrations/0029_destination_watershed_alliance.py create mode 100644 python/cac_tripplanner/destinations/migrations/0030_default_watershed_alliance.py diff --git a/python/cac_tripplanner/destinations/migrations/0029_destination_watershed_alliance.py b/python/cac_tripplanner/destinations/migrations/0029_destination_watershed_alliance.py new file mode 100644 index 000000000..06690f079 --- /dev/null +++ b/python/cac_tripplanner/destinations/migrations/0029_destination_watershed_alliance.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.7 on 2017-12-01 20:16 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('destinations', '0028_default_activities'), + ] + + operations = [ + migrations.AddField( + model_name='destination', + name='watershed_alliance', + field=models.BooleanField(default=False, help_text=(b'Does this location belong ', b'to the Alliance for Watershed Education?')), + ), + ] diff --git a/python/cac_tripplanner/destinations/migrations/0030_default_watershed_alliance.py b/python/cac_tripplanner/destinations/migrations/0030_default_watershed_alliance.py new file mode 100644 index 000000000..a76613883 --- /dev/null +++ b/python/cac_tripplanner/destinations/migrations/0030_default_watershed_alliance.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + + +def get_watershed_alliance_locations(): + """Names of default destinations added in migration 0012 in the Watershed Alliance + + All of them are in the watershed alliance: https://www.watershedalliance.org/centers/ + """ + return [ + 'Bartram\'s Garden', + 'Fairmount Waterworks Interpretive Center', + 'Independence Seaport Museum', + 'John Heinz National Wildlife Refuge', + 'NJ Academy of Aquatic Sciences', + 'Palmyra Cove Nature Park', + 'Schuylkill Environmental Education Center', + 'Schuylkill River Greenway Association', + 'Tulpehaking Nature Center at Abbott Marshland', + ] + +def set_watershed_alliance(apps, schema_editor): + Destination = apps.get_model('destinations', 'Destination') + + # set the watershed alliance flag on the default destinations that were added in migration 0012 + for dest in get_watershed_alliance_locations(): + destination = Destination.objects.get(name=dest) + if not destination: + continue + destination.watershed_alliance = True + destination.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('destinations', '0029_destination_watershed_alliance'), + ] + + operations = [ + migrations.RunPython(set_watershed_alliance, migrations.RunPython.noop), + ] diff --git a/python/cac_tripplanner/destinations/models.py b/python/cac_tripplanner/destinations/models.py index 1c327d84a..1df728207 100644 --- a/python/cac_tripplanner/destinations/models.py +++ b/python/cac_tripplanner/destinations/models.py @@ -86,6 +86,10 @@ class Meta: 'but may be overridden manually if incorrect.')) point = models.PointField() categories = models.ManyToManyField('DestinationCategory') + watershed_alliance = models.BooleanField(default=False, help_text=""" + Does this location belong to the + Alliance for Watershed Education?""") objects = DestinationManager() From ce42e50a005e743fff5045212ec79648c0926eb6 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Fri, 1 Dec 2017 15:42:08 -0500 Subject: [PATCH 31/31] Serialize activities by name Serialize destination activities to JSON by name. --- python/cac_tripplanner/destinations/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/cac_tripplanner/destinations/views.py b/python/cac_tripplanner/destinations/views.py index cd9b2e510..7f78bd74f 100644 --- a/python/cac_tripplanner/destinations/views.py +++ b/python/cac_tripplanner/destinations/views.py @@ -143,6 +143,7 @@ def set_destination_properties(destination): 'StAddr': obj['address'] } obj['categories'] = [c.name for c in obj['categories']] + obj['activities'] = [a.name for a in obj['activities']] return obj