Commit 88f9728d authored by David Haynes's avatar David Haynes 🙆
Browse files

Merge branch '51-nix-stale-schedules' into 'master'

Resolve "Do not serve schedules that have outdated times"

Closes #51

See merge request !24
parents c7d8c07d 9000f5af
......@@ -3,7 +3,6 @@ until nc -z wopen_db 3306; do
sleep 1
done
export WOPEN_SECRET_KEY=$(dd if=/dev/urandom count=100 | tr -dc "A-Za-z0-9" | fold -w 60 | head -n1 2>/dev/null)
python whats-open/manage.py flush --no-input
......
......@@ -148,6 +148,16 @@ class Facility(TimeStampedModel):
# Closed
return False
def clean_special_schedules(self):
"""
Loop through every special_schedule and remove entries that have
expired.
"""
for special_schedule in self.special_schedules.all():
# If it ends before today
if special_schedule.valid_end < datetime.date.today():
self.special_schedules.remove(special_schedule)
class Meta:
verbose_name = "facility"
verbose_name_plural = "facilities"
......
......@@ -15,8 +15,7 @@ from .models import Category, Facility, Schedule, OpenTime, Location, Alert
# Other Imports
from rest_framework import serializers
from taggit_serializer.serializers import (TagListSerializerField,
TaggitSerializer)
from taggit_serializer.serializers import TagListSerializerField
class AlertSerializer(serializers.ModelSerializer):
"""
......
......@@ -24,11 +24,11 @@ from rest_framework.routers import DefaultRouter
ROUTER = DefaultRouter()
# Register views to the API router
ROUTER.register(r'categories', CategoryViewSet)
ROUTER.register(r'facilities', FacilityViewSet)
ROUTER.register(r'schedules', ScheduleViewSet)
ROUTER.register(r'locations', LocationViewSet)
ROUTER.register(r'alerts', AlertViewSet)
ROUTER.register(r'categories', CategoryViewSet, 'category')
ROUTER.register(r'facilities', FacilityViewSet, 'facility')
ROUTER.register(r'schedules', ScheduleViewSet, 'schedule')
ROUTER.register(r'locations', LocationViewSet, 'location')
ROUTER.register(r'alerts', AlertViewSet, 'alert')
urlpatterns = [
# / - Default route
......
......@@ -7,10 +7,13 @@ Rest Framework Class Views
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# Python std. lib. imports
import datetime
# App Imports
from .models import Facility, OpenTime, Category, Schedule, Location, Alert
from .serializers import (CategorySerializer, FacilitySerializer,
ScheduleSerializer, OpenTimeSerializer,
ScheduleSerializer, OpenTimeSerializer,
LocationSerializer, AlertSerializer)
# Other Imports
......@@ -18,50 +21,113 @@ from rest_framework import viewsets
class AlertViewSet(viewsets.ReadOnlyModelViewSet):
"""
Return all Alert objects.
"""
queryset = Alert.objects.all()
serializer_class = AlertSerializer
def get_queryset(self):
"""
Handle incoming GET requests and enumerate objects that get returned by
the API.
"""
return Alert.objects.all()
class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
"""
Return all Category objects.
"""
queryset = Category.objects.all()
serializer_class = CategorySerializer
def get_queryset(self):
"""
Handle incoming GET requests and enumerate objects that get returned by
the API.
"""
return Category.objects.all()
class LocationViewSet(viewsets.ReadOnlyModelViewSet):
"""
Return all Location objects.
"""
queryset = Location.objects.all()
serializer_class = LocationSerializer
def get_queryset(self):
"""
Handle incoming GET requests and enumerate objects that get returned by
the API.
"""
return Location.objects.all()
class FacilityViewSet(viewsets.ReadOnlyModelViewSet):
"""
A Facility is some type of establishment that has a schedule of open hours and a location that serves a specific purpose that can be categorized.
GET /api/facilities/
Return all Facility objects. We additionally filter out stale special_schedules to reduce client side calculations.
GET /api/facilities/?open_now
Query parameter that only returns open Facility objects.
"""
queryset = Facility.objects.all()
serializer_class = FacilitySerializer
def get_queryset(self):
"""
Handle incoming GET requests and enumerate objects that get returned by
the API.
"""
queryset = Facility.objects.all()
open_now = self.request.query_params.get('open', None)
# Define and handle ?open_now
open_now = self.request.query_params.get('open_now', None)
if open_now is not None:
results = []
for fac in queryset:
if fac.is_open_now():
results.append(fac)
return results
# List of all open facilities
open_facilities = []
for facility in Facility.objects.all():
if facility.is_open():
# Append the primary key
open_facilities.append(facility.pk)
# Return all Facility objects with the primary keys located in the
# open_facilities list
return Facility.objects.filter(pk__in=open_facilities)
# Default behavior
else:
return queryset
for facility in Facility.objects.all():
# Remove all special_schedules that have expired
facility.clean_special_schedules()
return Facility.objects.all()
class ScheduleViewSet(viewsets.ModelViewSet):
"""
A period of time between two dates that represents the beginning and end of a "schedule" or rather, a collection of open times for a facility.
GET /api/schedules
Return all Schedule objects that have not expired. (ie. end_date is before today)
"""
queryset = Schedule.objects.all()
serializer_class = ScheduleSerializer
def get_queryset(self):
"""
Handle incoming GET requests and enumerate objects that get returned by
the API.
"""
# List of all schedules that are outdated
filter_old_schedules = []
for schedule in Schedule.objects.all():
if schedule.valid_end and schedule.valid_start:
# If the schedule ended before today
if schedule.valid_end < datetime.date.today():
# Add it to the list of objects we are excluding
filter_old_schedules.append(schedule.pk)
# Return all Schedule objects that have not expired
return Schedule.objects.exclude(pk__in=filter_old_schedules)
class OpenTimeViewSet(viewsets.ModelViewSet):
"""
Return all OpenTime objects.
"""
queryset = OpenTime.objects.all()
serializer_class = OpenTimeSerializer
def get_queryset(self):
"""
Handle incoming GET requests and enumerate objects that get returned by
the API.
"""
return OpenTime.objects.all()
\ No newline at end of file
"""
manage.py
Do not touch at all. This is the main entry point for interacting with Django.
"""
#!/usr/bin/env python
import os
import sys
......
""""Base Django settings for whats_open."""
""""
settings/base.py
Base Django settings for whats-open.
"""
# Future Imports
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# Python std. lib. imports
import os
import sys
from os.path import abspath, basename, dirname, join, normpath
from os.path import abspath, dirname, join, normpath
from sys import path
########## PATH CONFIGURATION
"""
PATH CONFIGURATION
"""
# Absolute filesystem path to the Django project directory:
DJANGO_ROOT = dirname(dirname(abspath(__file__)))
......@@ -15,9 +25,10 @@ SITE_ROOT = dirname(DJANGO_ROOT)
# Add our project to our pythonpath, this way we don't need to type our project
# name in our dotted import paths:
path.append(DJANGO_ROOT)
########## END PATH CONFIGURATION
########## MANAGER CONFIGURATION
"""
MANAGER CONFIGURATION
"""
# See: https://docs.djangoproject.com/en/dev/ref/settings/#admins
# Insert a ('Name', 'Email') inside ADMINS tuple
ADMINS = (
......@@ -26,9 +37,10 @@ ADMINS = (
# See: https://docs.djangoproject.com/en/dev/ref/settings/#managers
MANAGERS = ADMINS
########## END MANAGER CONFIGURATION
########## GENERAL CONFIGURATION
"""
GENERAL CONFIGURATION
"""
# See: https://docs.djangoproject.com/en/dev/ref/settings/#time-zone
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
......@@ -56,10 +68,10 @@ USE_L10N = True
# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-tz
# If you set this to False, Django will not use timezone-aware datetimes.
USE_TZ = True
########## END GENERAL CONFIGURATION
########## MEDIA CONFIGURATION
"""
MEDIA CONFIGURATION
"""
# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root
# Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/home/media/media.lawrence.com/media/"
......@@ -70,26 +82,22 @@ MEDIA_ROOT = normpath(join(SITE_ROOT, 'media'))
# trailing slash.
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
MEDIA_URL = '/media/'
########## END MEDIA CONFIGURATION
########## STATIC FILE CONFIGURATION
"""
STATIC FILE CONFIGURATION
"""
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root
# Absolute path to the directory static files should be collected to.
# Don't put anything in this directory yourself; store your static files
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/home/media/media.lawrence.com/static/"
STATIC_ROOT = normpath(join(SITE_ROOT, 'assets'))
# Example: "/home/media/media.lawrence.com/static/"
STATIC_ROOT = normpath(join(SITE_ROOT, 'static'))
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url
# URL prefix for static files.
# Example: "http://media.lawrence.com/static/"
STATIC_URL = '/static/'
# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
STATICFILES_DIRS = (
normpath(join(SITE_ROOT, 'static')),
)
ADMIN_MEDIA_PREFIX = '/static/admin/'
# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders
......@@ -97,31 +105,34 @@ STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
)
########## END STATIC FILE CONFIGURATION
########## SECRET CONFIGURATION
"""
SECRET CONFIGURATION
"""
# See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
# Note: This key should only be used for development and testing.
SECRET_KEY = r"{{ secret_key }}"
########## END SECRET CONFIGURATION
########## SITE CONFIGURATION
"""
SITE CONFIGURATION
"""
# Hosts/domain names that are valid for this site; required if DEBUG is False
# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts
ALLOWED_HOSTS = ['*']
########## END SITE CONFIGURATION
########## FIXTURE CONFIGURATION
"""
FIXTURE CONFIGURATION
"""
# See: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FIXTURE_DIRS
FIXTURE_DIRS = (
normpath(join(SITE_ROOT, 'fixtures')),
)
########## END FIXTURE CONFIGURATION
########## DATABASE CONFIGURATION
"""
DATABASE CONFIGURATION
"""
# Use the same DB everywhere.
# See: https://docs.djangoproject.com/en/dev/ref/settings/#databases
DATABASES = {
......@@ -135,7 +146,9 @@ DATABASES = {
}
}
########## TEMPLATE CONFIGURATION
"""
TEMPLATE CONFIGURATION
"""
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
......@@ -165,10 +178,10 @@ TEMPLATES = [
}
}
]
########## END TEMPLATE CONFIGURATION
########## MIDDLEWARE CONFIGURATION
"""
MIDDLEWARE CONFIGURATION
"""
# See: https://docs.djangoproject.com/en/dev/ref/settings/#middleware-classes
MIDDLEWARE_CLASSES = (
# Default Django middleware.
......@@ -179,36 +192,36 @@ MIDDLEWARE_CLASSES = (
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
########## END MIDDLEWARE CONFIGURATION
########## URL CONFIGURATION
"""
URL CONFIGURATION
"""
# See: https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf
ROOT_URLCONF = 'settings.urls'
########## END URL CONFIGURATION
########## WSGI CONFIGURATION
"""
WSGI CONFIGURATION
"""
# See: https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application
# Python dotted path to the WSGI application used by Django's runserver.
WSGI_APPLICATION = 'settings.wsgi.application'
########## END WSGI CONFIGURATION
########## SERIALIZER CONFIGURATION
"""
SERIALIZER CONFIGURATION
"""
# http://djx.readthedocs.org/en/latest/topics/http/sessions.html#session-serialization
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer'
########## END SERIALIZER CONFIGURATION
########## CACHE MIDDLEWARE CONFIGURATION
"""
CACHE MIDDLEWARE CONFIGURATION
"""
CACHE_MIDDLEWARE_ALIAS = 'default'
CACHE_MIDDLEWARE_SECONDS = 259200
CACHE_MIDDLEWARE_KEY_PREFIX = ''
########## END CACHE MIDDLEWARE CONFIGURATION
########## APP CONFIGURATION
"""
APP CONFIGURATION
"""
INSTALLED_APPS = (
# Default Django apps:
'django.contrib.auth',
......@@ -233,10 +246,9 @@ INSTALLED_APPS = (
'rest_framework_gis',
)
########## END APP CONFIGURATION
########## DJANGO REST FRAMEWORK CONFIGURATION
"""
DJANGO REST FRAMEWORK CONFIGURATION
"""
# http://www.django-rest-framework.org/api-guide/settings
REST_FRAMEWORK = {
# Use hyperlinked styles by default.
......@@ -250,10 +262,10 @@ REST_FRAMEWORK = {
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
]
}
########## END DJANGO REST FRAMEWORK CONFIGURATION
########## LOGGING CONFIGURATION
"""
LOGGING CONFIGURATION
"""
# See: https://docs.djangoproject.com/en/dev/ref/settings/#logging
# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
......@@ -293,5 +305,3 @@ LOGGING = {
},
}
}
########## END LOGGING CONFIGURATION
"""Development settings and globals."""
"""
settings/local.py
from __future__ import absolute_import
Development settings and globals.
"""
# Future Imports
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# Import the base settings and override where necessary
from .base import *
########## DEBUG CONFIGURATION
"""
DEBUG CONFIGURATION
"""
# See: https://docs.djangoproject.com/en/dev/ref/settings/#debug
DEBUG = True
########## END DEBUG CONFIGURATION
########## CACHE CONFIGURATION
"""
CACHE CONFIGURATION
"""
# See: https://docs.djangoproject.com/en/dev/ref/settings/#caches
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
}
}
########## END CACHE CONFIGURATION
"""Production settings and globals."""
"""
settings/production.py
Production settings and globals.
"""
# Future Imports
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# Python std. lib imports
from os import environ
from .base import *
# Normally you should not import ANYTHING from Django directly
# into your settings, but ImproperlyConfigured is an exception.
# Django Imports
from django.core.exceptions import ImproperlyConfigured
# Import the base settings and override where necessary
from .base import *
def get_env_setting(setting):
""" Get the environment setting or return exception """
"""
Get the environment setting or return exception
"""
try:
return environ[setting]
except KeyError as ex:
error_msg = "Set the %s env variable" % setting
raise ImproperlyConfigured(error_msg)
########## HOST CONFIGURATION
"""
HOST CONFIGURATION
"""
# See: https://docs.djangoproject.com/en/1.5/releases/1.5/#allowed-hosts-required-in-production
ALLOWED_HOSTS = ['*']
########## END HOST CONFIGURATION
########## DEBUG CONFIGURATION
"""
DEBUG CONFIGURATION
"""
# See: https://docs.djangoproject.com/en/dev/ref/settings/#debug
DEBUG = False
########## END DEBUG CONFIGURATION
########## CACHE CONFIGURATION
"""
CACHE CONFIGURATION
"""
# See: https://docs.djangoproject.com/en/dev/ref/settings/#caches
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
}
}
########## END CACHE CONFIGURATION
########## SECRET CONFIGURATION
"""
SECRET CONFIGURATION
"""
# See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
SECRET_KEY = get_env_setting('WOPEN_SECRET_KEY')
########## END SECRET CONFIGURATION
########## END SECRET CONFIGURATION
\ No newline at end of file
"""
settings/urls.py
Top level url patterns.
"""
# Future Imports
from __future__ import (absolute_import, division, print_function,
unicode_literals)
......@@ -7,10 +12,12 @@ from django.conf.urls import include, url
from django.contrib import admin
import django.contrib.auth.views
# Automatically populate the admin pages
admin.autodiscover()
# Define all the top level url patterns in a list
urlpatterns = [
# / - The homepage
# / - Load in all urls from the `api` app
url(r'^', include('api.urls')),
# /admin - The admin panels
......
"""
settings/wsgi.py
WSGI config for whats_open project.
This module contains the WSGI application used by Django's development server
......@@ -11,7 +13,6 @@ might make sense to replace the whole Django WSGI application with a custom one
that later delegates to the Django one. For example, you could introduce WSGI
middleware here, or combine a Django application with an application of another
framework.
"""
import os
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment