From e545b9d741bb2e0b917d91bef4480451832c2a33 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 29 May 2015 17:27:37 +0100 Subject: [PATCH 1/9] remove comma --- app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.json b/app.json index adf910b..50ef2a8 100644 --- a/app.json +++ b/app.json @@ -5,7 +5,7 @@ "keywords": [ "django", "arduino", "REST", "sensor" ], "addons": [ "heroku-postgresql:hobby-dev", - "heroku addons:create sendgrid:starter", + "heroku addons:create sendgrid:starter" ], "env": { "DJANGO_SECRET_KEY": { From 16d38acaad7ec7a2e507178c15683a89597a3df2 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 29 May 2015 17:34:27 +0100 Subject: [PATCH 2/9] change addon string --- app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.json b/app.json index 50ef2a8..e2542d8 100644 --- a/app.json +++ b/app.json @@ -5,7 +5,7 @@ "keywords": [ "django", "arduino", "REST", "sensor" ], "addons": [ "heroku-postgresql:hobby-dev", - "heroku addons:create sendgrid:starter" + "sendgrid:starter" ], "env": { "DJANGO_SECRET_KEY": { From 30c6431fdbdf881a9092fade28bde6594caa1a7f Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Fri, 29 May 2015 17:45:26 +0100 Subject: [PATCH 3/9] change addon strings --- app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app.json b/app.json index e2542d8..8ed0954 100644 --- a/app.json +++ b/app.json @@ -4,8 +4,8 @@ "repository": "https://github.com/developmentseed/dustduino-server", "keywords": [ "django", "arduino", "REST", "sensor" ], "addons": [ - "heroku-postgresql:hobby-dev", - "sendgrid:starter" + "heroku-postgresql", + "sendgrid" ], "env": { "DJANGO_SECRET_KEY": { From 6e812bf20ce8aebf55c08c4f4d2e015722b24095 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sat, 30 May 2015 17:24:39 +0000 Subject: [PATCH 4/9] Import source code --- .build_scripts/deploy.sh | 21 ++ .gitignore | 61 +++++ .travis.yml | 20 ++ LICENSE | 28 ++ Procfile | 1 + README.md | 38 +++ app.json | 17 ++ docs/docs.md | 162 +++++++++++ requirements.txt | 1 + requirements/base.txt | 15 + sensor_rest_api/__init__.py | 0 sensor_rest_api/api/__init__.py | 0 sensor_rest_api/api/permissions.py | 31 +++ sensor_rest_api/api/tests.py | 3 + sensor_rest_api/api/v1/__init__.py | 0 sensor_rest_api/api/v1/mailer.py | 80 ++++++ sensor_rest_api/api/v1/serializers.py | 22 ++ sensor_rest_api/api/v1/urls.py | 12 + sensor_rest_api/api/v1/views.py | 180 ++++++++++++ sensor_rest_api/config/__init__.py | 5 + sensor_rest_api/config/base.py | 132 +++++++++ sensor_rest_api/config/local.py | 14 + sensor_rest_api/config/production.py | 22 ++ sensor_rest_api/manage.py | 37 +++ sensor_rest_api/sensors/__init__.py | 0 .../sensors/migrations/0001_initial.py | 51 ++++ .../migrations/0002_sensorverification.py | 29 ++ .../migrations/0003_reading_hour_code.py | 20 ++ .../sensors/migrations/__init__.py | 0 sensor_rest_api/sensors/models.py | 51 ++++ .../sensors/templates/sensors/base.html | 15 + .../sensors/templates/sensors/error.html | 11 + .../sensors/templates/sensors/verify.html | 13 + sensor_rest_api/sensors/urls.py | 7 + sensor_rest_api/sensors/views.py | 56 ++++ .../templates/rest_framework/base.html | 257 ++++++++++++++++++ sensor_rest_api/urls.py | 9 + sensor_rest_api/wsgi.py | 16 ++ 38 files changed, 1437 insertions(+) create mode 100755 .build_scripts/deploy.sh create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 Procfile create mode 100644 README.md create mode 100644 app.json create mode 100644 docs/docs.md create mode 100644 requirements.txt create mode 100644 requirements/base.txt create mode 100644 sensor_rest_api/__init__.py create mode 100644 sensor_rest_api/api/__init__.py create mode 100644 sensor_rest_api/api/permissions.py create mode 100644 sensor_rest_api/api/tests.py create mode 100644 sensor_rest_api/api/v1/__init__.py create mode 100644 sensor_rest_api/api/v1/mailer.py create mode 100644 sensor_rest_api/api/v1/serializers.py create mode 100644 sensor_rest_api/api/v1/urls.py create mode 100644 sensor_rest_api/api/v1/views.py create mode 100644 sensor_rest_api/config/__init__.py create mode 100644 sensor_rest_api/config/base.py create mode 100644 sensor_rest_api/config/local.py create mode 100644 sensor_rest_api/config/production.py create mode 100755 sensor_rest_api/manage.py create mode 100644 sensor_rest_api/sensors/__init__.py create mode 100644 sensor_rest_api/sensors/migrations/0001_initial.py create mode 100644 sensor_rest_api/sensors/migrations/0002_sensorverification.py create mode 100644 sensor_rest_api/sensors/migrations/0003_reading_hour_code.py create mode 100644 sensor_rest_api/sensors/migrations/__init__.py create mode 100644 sensor_rest_api/sensors/models.py create mode 100644 sensor_rest_api/sensors/templates/sensors/base.html create mode 100644 sensor_rest_api/sensors/templates/sensors/error.html create mode 100644 sensor_rest_api/sensors/templates/sensors/verify.html create mode 100644 sensor_rest_api/sensors/urls.py create mode 100644 sensor_rest_api/sensors/views.py create mode 100644 sensor_rest_api/templates/rest_framework/base.html create mode 100644 sensor_rest_api/urls.py create mode 100644 sensor_rest_api/wsgi.py diff --git a/.build_scripts/deploy.sh b/.build_scripts/deploy.sh new file mode 100755 index 0000000..0e8b1eb --- /dev/null +++ b/.build_scripts/deploy.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -e # halt script on error + +# If this is the publish branch, push it up to gh-pages +if [ $TRAVIS_PULL_REQUEST = "false" ] && [ $TRAVIS_BRANCH = "master" ]; then + echo "Get ready, we're publishing!" + npm install -g aglio + mkdir -p dist + aglio -t slate -i docs/docs.md -o dist/index.html + cd dist + echo ".DS_Store" > .gitignore + git init + git config user.name "Travis-CI" + git config user.email "travis@somewhere.com" + git add . + git commit -m "CI deploy to gh-pages" + git push --force --quiet "https://${GH_TOKEN}@${GH_REF}" master:gh-pages + rm -rf .git/ +else + echo "Not a publishable branch so we're all done here" +fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc2abd0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +SECRET_KEY.txt +db.sqlite3 +api/migrations + +venv +.env + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..dfd8e16 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: python +python: +- '2.7' + +branches: + only: + - master + +env: + global: + - GH_REF=github.com/developmentseed/dustduino-server.git + - secure: CbzgND/x3QGeJmVIpLpjuHMn9SFuRkpyWL65KjLanM7k+h86JfmeYaOxnL/6yXYeh6klzYOq8lBQQZayfC9Pre0acrucNB0vyRBLfoW7zeUWeIuQ9Nz1JpjDNNtovDz19mpP8JxzsvzoWdIsfvT8hfeN1/P2a5V+lzZGn50c/9I= + +install: echo 'no install' + +script: echo 'no scripts' + +before_install: chmod +x ./.build_scripts/deploy.sh + +after_success: ./.build_scripts/deploy.sh diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..23e4bb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2014, Development Seed +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of sensor-rest-api nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..70f20f8 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: gunicorn --pythonpath="$PWD/sensor_rest_api" wsgi:application diff --git a/README.md b/README.md new file mode 100644 index 0000000..9d6c4da --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ + +A REST API for DustDuino air quality sensors + +[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) + +For API documentation [click here](http://devseed.com/dustduino-server/). + +## Deployment to Heroku + +Use the heroku button above or manually create an app on heroku and deploy. For configuration purposes, the following table maps environment variables to their Django setting: + +|Environment Variable |Django Setting |Development Default |Production Default +| -------------------------------------- | -------------------------- | --------------------------- | ----------------- +|DJANGO_SECRET_KEY |SECRET_KEY |CHANGEME!!! |raises error +|DJANGO_PORTAL_URL |PORTAL_URL |http://127.0.0.1:8000 |http://127.0.0.1:8000 + +## Installation on local machine + +Create a virtual environment +``` +virtualenv venv +source venv/bin/activate +``` + +Install the requirements +``` +pip install -r requirements.txt +``` + +Initialize the database +``` +python sensor_rest_api/manage.py syncdb +``` + +Start the server +``` +python sensor_rest_api/manage.py runserver +``` diff --git a/app.json b/app.json new file mode 100644 index 0000000..cb93d8c --- /dev/null +++ b/app.json @@ -0,0 +1,17 @@ +{ + "name": "Sensor REST Api", + "description": "REST API for dustDuino air quality sensors", + "repository": "https://github.com/smit1678/dustduino-server", + "keywords": [ "django", "arduino", "REST", "sensor" ], + "addons": [ + "heroku-postgresql", + "sendgrid" + ], + "env": { + "DJANGO_SECRET_KEY": { + "description": "Salt for hashing password", + "required": true, + "generator": "secret" + } + } +} diff --git a/docs/docs.md b/docs/docs.md new file mode 100644 index 0000000..864ac5e --- /dev/null +++ b/docs/docs.md @@ -0,0 +1,162 @@ +FORMAT: 1A +HOST: https://brazil-sensor.herokuapp.com + +# A REST API for Sensor Data + +[This REST API](https://brazil-sensor.herokuapp.com/api/v1/) collects data from dustDuino air sensors and allow users to query them. + +# The API Root [/api/v1] + +This resource does not have any attributes. Instead it offers the initial +API affordances in the form of the links in the JSON body. + +It is recommend to follow the “url” link values, +[Link](https://tools.ietf.org/html/rfc5988) or Location headers where +applicable to retrieve resources. Instead of constructing your own URLs, +to keep your client decoupled from implementation details. + +## Retrieve the Entry Point [GET] + ++ Response 200 (application/json) + + { + "readings": "http://brazil-sensor.herokuapp.com/api/v1/readings/", + "sensors": "http://brazil-sensor.herokuapp.com/api/v1/sensors/" + } + +## Readings [/api/v1/readings/{?sensor_id,email,start,end}] + +### List readings + +Returns all readings from sensors. Please note that the readings are returned per hour. Each fields is the average value of readings during the hour. + +A reading object has the following attributes: + ++ `pm10` - Particulate matter smaller than about 10 micrometers ++ `pm25` - Particulate matter smaller than about 25 micrometers ++ `pm10count` - Particulate matter counter for paricles smaller than about 10 micrometers ++ `pm25count` - Particulate matter counter for paricles smaller than about 25 micrometers ++ `sensor` - Sensor ID ++ `hour_code` - The hour in which the reading average is calculated | format: YYYYMMDDHH + +### Retrieve Readings List [GET] + ++ Parameters + + sensor_id (optional, number, `1`) ... ID for a particular sensor + + email (optional, string, `email@example.com`) ... email address with which the sensor is registered + + start (optional, date, `2014-12-31`) ... The start date for readings | format: YYYY-MM-DD + + end (optional, date, `2015-12-31`) ... The end date for readings | format: YYYY-MM-DD + ++ Response 200 (application/json) + + { + "count": 3, + "next": null, + "previous": null, + "results": [ + { + "pm25count": 0.0, + "pm10": 1.0, + "pm10count": 3.0, + "hour_code": "2015042818", + "sensor": 2, + "pm25": 2.0 + }, + { + "pm25count": 7.0, + "pm10": 6.0, + "pm10count": 3.0, + "hour_code": "2015042819", + "sensor": 2, + "pm25": 2.0 + }, + { + "pm25count": 100.0, + "pm10": 1.0, + "pm10count": 300.0, + "hour_code": "2015042820", + "sensor": 4, + "pm25": 45.0 + } + ] + } + +### Create a New Reading [POST] + ++ Parameters + + pm10 (optional, number, `1`) ... Particulate matter smaller than about 10 micrometers + + pm25 (optional, number, `1`) ... Particulate matter smaller than about 25 micrometers + + pm10count (optional, number, `1`) ... Particulate matter counter for paricles smaller than about 10 micrometers + + pm25count (optional, number, `1`) ... Particulate matter counter for paricles smaller than about 25 micrometers + ++ Request + + + Header + + Authorization: Token yourtoken + ++ Response 200 (application/json) + + { + "id": 1, + "created": "2015-04-28T19:43:20.141296Z", + "hour_code": "2015042819", + "pm10": 12, + "pm25": 0, + "pm10count": 100, + "pm25count": 130, + "sensor": 3 + } + + +## Sensors [/api/v1/sensors/{sensor_id}] + +Returns the list of active sensors. + +A sensor object has the following attributes: + ++ `id` - unique sensor ID ++ `sensor_name` - sensor's name ++ `lat` - sensor's latitude ++ `lon` - sensor's longitude ++ `address` - sensor's address ++ `serial` - sensor's serial number ++ `description` - sensor's description ++ `account` - user account associated with the sensor ++ `last_reading` - the latest reading object + +### View Sensors List [GET] + ++ Parameters + + sensor_id (optional, number, `1`) ... sensor unique ID + + ++ Response 200 (application/json) + + { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 2, + "sensor_name": "jumpy-bronze-mongrel", + "lat": -23.55187083133668, + "lon": -46.65361404418945, + "address": null, + "serial": null, + "description": "Enter a description of your device", + "account": 7, + "last_reading": { + "sensor_id": 2, + "pm25count": 12, + "pm10": 10, + "created": "2015-04-28T19:52:17.836Z", + "pm10count": 3, + "hour_code": "2015042819", + "id": 3, + "pm25": 2 + } + } + ] + } diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5603c37 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +-r requirements/base.txt diff --git a/requirements/base.txt b/requirements/base.txt new file mode 100644 index 0000000..25fabba --- /dev/null +++ b/requirements/base.txt @@ -0,0 +1,15 @@ +Django==1.7.7 +dj-database-url==0.3.0 +django-configurations==0.8 +dj-static==0.0.6 +django-cors-headers==0.13 +django-finalware==0.0.8 +django-toolbelt==0.0.1 +djangorestframework==3.1.1 +gunicorn==19.3 +psycopg2==2.6 +pytz==2014.7 +static3==0.5.1 +wsgiref==0.1.2 +random_name==0.1.0 +python-dateutil==2.4.2 diff --git a/sensor_rest_api/__init__.py b/sensor_rest_api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sensor_rest_api/api/__init__.py b/sensor_rest_api/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sensor_rest_api/api/permissions.py b/sensor_rest_api/api/permissions.py new file mode 100644 index 0000000..6d83eba --- /dev/null +++ b/sensor_rest_api/api/permissions.py @@ -0,0 +1,31 @@ +from rest_framework import permissions + +class IsOwnerOrReadOnly(permissions.BasePermission): + """ + Custom permission to only allow owners of an object to edit it. + """ + def has_object_permission(self, request, view, obj): + # Read permissions are allowed to any request + # So we'll always allow GET, HEAD, or OPTIONS request. + if request.method in permissions.SAFE_METHODS: + return True + # Write permissions are only allowed to the owner of the snippet. + return (obj.owner == request.user or request.user.is_staff) + +class IsUserOrReadOnly(permissions.BasePermission): + """ + Custom permission to only allow user to update own object + """ + def has_permission(self, request, view): + # allow user to list all users if logged in user is staff + if view.action in ['retrieve', 'update', 'destroy']: + return True + return request.user.is_staff + + def has_object_permission(self, request, view, obj): + # Read permissions are allowed to any request + # So we'll always allow GET, HEAD, or OPTIONS request. + if request.method in permissions.SAFE_METHODS: + return True + # Write permissions are only allowed to the owner of the snippet. + return (obj == request.user or request.user.is_staff) diff --git a/sensor_rest_api/api/tests.py b/sensor_rest_api/api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/sensor_rest_api/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/sensor_rest_api/api/v1/__init__.py b/sensor_rest_api/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sensor_rest_api/api/v1/mailer.py b/sensor_rest_api/api/v1/mailer.py new file mode 100644 index 0000000..5082414 --- /dev/null +++ b/sensor_rest_api/api/v1/mailer.py @@ -0,0 +1,80 @@ +from os.path import join + +from django.core.mail import EmailMultiAlternatives +from django.conf import settings + + +class VerificaitonEmail(object): + def __init__(self, to_email, code, **kwargs): + self.to_email = to_email + self.from_email = settings.DEFAULT_FROM_EMAIL + self.code = code + self.site_url = settings.PORTAL_URL + + def verify_link(self): + return '%s?code=%s' % (join(self.site_url, 'verify'), self.code) + + def buildText(self): + text = 'Hi, \n\n' + text += 'Your request to register a new sensor is received. Please verify this request by clicking ' + text += 'on the below link.\n\n' + text += self.verify_link() + text += '\n\nThank you!' + + return text + + def buildHTML(self): + text = '

Hi,

' + text += '

Your request to register a new sensor is received. Please verify this request ' + text += 'by clicking on the below link.

' + text += '

%s

' % (self.verify_link(), self.verify_link()) + text += '

Thank you!

' + + return text + + def send(self): + from_email = self.from_email + subject = 'Verify The Sensor' + text_content = self.buildText() + html_content = self.buildHTML() + + msg = EmailMultiAlternatives(subject, text_content, from_email, + [self.to_email]) + msg.attach_alternative(html_content, "text/html") + msg.send() + + +class TokenEmail(object): + def __init__(self, to_email, token, sensor_name=None): + self.to_email = to_email + self.token = token + self.sensor_name = sensor_name + self.from_email = settings.DEFAULT_FROM_EMAIL + self.site_url = settings.PORTAL_URL + + def buildText(self): + text = 'Hi, \n\n' + text += 'The API Token is %s \n\n' % self.token + text += 'This sensor is registered using %s.\n\n' % self.to_email + text += 'Thank you!' + + return text + + def buildHTML(self): + text = '

Hi,

' + text += '

The API Token is %s

' % self.token + text += '

This sensor is registered with %s.

' % self.to_email + text += '

Thank you!

' + + return text + + def send(self): + from_email = self.from_email + subject = 'API Token for sensor named %s' % self.sensor_name + text_content = self.buildText() + html_content = self.buildHTML() + + msg = EmailMultiAlternatives(subject, text_content, from_email, + [self.to_email]) + msg.attach_alternative(html_content, "text/html") + msg.send() diff --git a/sensor_rest_api/api/v1/serializers.py b/sensor_rest_api/api/v1/serializers.py new file mode 100644 index 0000000..d3fdad9 --- /dev/null +++ b/sensor_rest_api/api/v1/serializers.py @@ -0,0 +1,22 @@ +from rest_framework import serializers + +from sensors.models import Reading, Sensor + + +class ReadingSerializer(serializers.ModelSerializer): + class Meta: + model = Reading + fields = ['sensor', 'hour_code', 'pm10', 'pm25', 'pm10count', 'pm25count'] + + +class SensorSerializer(serializers.ModelSerializer): + + def to_representation(self, obj): + context = super(SensorSerializer, self).to_representation(obj) + + context['last_reading'] = Reading.objects.filter(sensor_id=obj.id).order_by('-created').values().first() + + return context + + class Meta: + model = Sensor diff --git a/sensor_rest_api/api/v1/urls.py b/sensor_rest_api/api/v1/urls.py new file mode 100644 index 0000000..6b76cf1 --- /dev/null +++ b/sensor_rest_api/api/v1/urls.py @@ -0,0 +1,12 @@ +from django.conf.urls import url, include +from api.v1.views import ReadingViewSet, SensorViewSet, register_sensor +from rest_framework.routers import DefaultRouter + +router = DefaultRouter() +router.register(r'readings', ReadingViewSet) +router.register(r'sensors', SensorViewSet) + +urlpatterns = [ + url(r'^', include(router.urls)), + url(r'^register/$', register_sensor) +] diff --git a/sensor_rest_api/api/v1/views.py b/sensor_rest_api/api/v1/views.py new file mode 100644 index 0000000..44a2e2b --- /dev/null +++ b/sensor_rest_api/api/v1/views.py @@ -0,0 +1,180 @@ +from django.db.models import Avg +from django.db import transaction +from django.contrib.auth.models import User +from django.core.validators import validate_email +from django.core.exceptions import ValidationError + +from rest_framework import viewsets, status +from rest_framework.response import Response +from rest_framework.decorators import api_view +from rest_framework.permissions import IsAuthenticatedOrReadOnly +import random_name +from dateutil.parser import parse + +from sensors.models import Reading, Sensor, SensorVerification +from api.v1.serializers import SensorSerializer, ReadingSerializer +from api.v1.mailer import VerificaitonEmail + + +class ReadingViewSet(viewsets.ModelViewSet): + queryset = Reading.objects.all() + serializer_class = ReadingSerializer + permission_classes = (IsAuthenticatedOrReadOnly,) + + def create(self, request, *args, **kwargs): + sensor = Sensor.objects.get(account=request.user) + request.data.__setitem__('sensor', sensor.id) + + print request.data + return super(ReadingViewSet, self).create(request, *args, **kwargs) + + def list(self, request, *args, **kwargs): + queryset = self.filter_queryset(self.get_queryset()) + + sensor_id = request.GET.get('sensor_id', None) + email = request.GET.get('email', None) + start = request.GET.get('start', None) + end = request.GET.get('end', None) + + if sensor_id: + queryset = queryset.filter(sensor_id=sensor_id) + + if email: + queryset = queryset.filter(sensor__account__email=email) + + if start: + try: + start = parse(start) + queryset = queryset.filter(created__gte=start) + except ValueError: + pass + + if end: + try: + end = parse(end) + queryset = queryset.filter(created__lte=end) + except ValueError: + pass + + queryset = queryset.values('hour_code', 'sensor').order_by('-hour_code').annotate(pm10=Avg('pm10'), + pm25=Avg('pm25'), + pm10count=Avg('pm10count'), + pm25count=Avg('pm25count')) + + page = self.paginate_queryset(queryset) + if page is not None: + return self.get_paginated_response(page) + + return Response(queryset) + + +class SensorViewSet(viewsets.ModelViewSet): + queryset = Sensor.objects.filter(account__is_active=True) + serializer_class = SensorSerializer + permission_classes = (IsAuthenticatedOrReadOnly,) + + def create(self, request, *args, **kwargs): + + return Response({"error": "Not Implemented"}, status=status.HTTP_400_BAD_REQUEST) + + def update(self, request, *args, **kwargs): + + email = request.data.pop('email') + if isinstance(email, list): + email = email[0] + + error = verify_email(email) + if error: + return error + + try: + user = User.objects.get(email=email) + + if user.is_active: + instance = Sensor.objects.get(account=user) + serializer = self.get_serializer(instance, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + return Response(serializer.data) + else: + return Response({"error": "The sensor associated with %s is not active." % email}, + status=status.HTTP_400_BAD_REQUEST) + except User.DoesNotExist: + return Response({"error": "The email (%s) does not exist" % email}, + status=status.HTTP_400_BAD_REQUEST) + + +def verify_email(email): + # If no email is provided raise an error + if email: + try: + validate_email(email) + return False + except ValidationError: + return Response({"error": "Provide a valid email address."}, + status=status.HTTP_400_BAD_REQUEST) + else: + return Response({"error": "Insufficient information provided."}, + status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +def register_sensor(request): + + if request.method == 'POST': + + email = request.POST.get('email') + if isinstance(email, list): + email = email[0] + + sensor_name = request.POST.get('sensor_name') + lat = request.POST.get('lat') + lon = request.POST.get('lon') + address = request.POST.get('address') + serial = request.POST.get('serial') + description = request.POST.get('description') + + error = verify_email(email) + if error: + return error + + if not sensor_name: + sensor_name = random_name.generate_name() + + # If a sensor with the email already exists issue an error + try: + user = User.objects.get(email=email) + + return Response({"error": "The email %s is already registered with a sensor" % email}, + status=status.HTTP_400_BAD_REQUEST) + + except User.DoesNotExist: + # Creat the new user + with transaction.atomic(): + username = User.objects.make_random_password(length=10) + password = User.objects.make_random_password(length=10) + + user = User(username=username, password=password, email=email, is_active=False) + user.save() + + # Add sensor information + sensor = Sensor(account=user, sensor_name=sensor_name, lat=lat, lon=lon, + address=address, serial=serial, description=description) + sensor.save() + + # Send Verification email + verification_code = User.objects.make_random_password(length=40) + verify = SensorVerification(account=user, verification_code=verification_code) + verify.save() + + mail = VerificaitonEmail(user.email, verification_code) + mail.send() + + return Response({ + 'email': user.email, + 'username': user.username, + 'id': user.id, + 'verified': verify.verified, + 'message': 'New sensor is registered. A verification email is sent to the email provided. ' + + 'Please verify the sensor.', + }, status=status.HTTP_201_CREATED) diff --git a/sensor_rest_api/config/__init__.py b/sensor_rest_api/config/__init__.py new file mode 100644 index 0000000..74406ab --- /dev/null +++ b/sensor_rest_api/config/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +from .local import Local # noqa +from .production import Production # noqa diff --git a/sensor_rest_api/config/base.py b/sensor_rest_api/config/base.py new file mode 100644 index 0000000..1200172 --- /dev/null +++ b/sensor_rest_api/config/base.py @@ -0,0 +1,132 @@ +""" +Django settings for sensor project. + +For more information on this file, see +https://docs.djangoproject.com/en/1.7/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.7/ref/settings/ +""" + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +import os +from os.path import join, dirname + +from configurations import Configuration, values + +BASE_DIR = dirname(dirname(__file__)) + + +class Base(Configuration): + + BASE_DIR = BASE_DIR + + PORTAL_URL = values.Value('http://127.0.0.1:8000') + DOCS_URL = values.Value('http://docs.sensorrestapi.apiary.io/#reference') + + INSTALLED_APPS = ( + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'corsheaders', + 'rest_framework', + 'rest_framework.authtoken', + 'api', + 'sensors', + 'finalware', + ) + + # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ + + SECRET_KEY = 'CHANGEME!!!' + + # SECURITY WARNING: don't run with debug turned on in production! + DEBUG = values.BooleanValue(False) + TEMPLATE_DEBUG = DEBUG + + MIDDLEWARE_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + ) + + TEMPLATE_DIRS = ( + join(BASE_DIR, 'templates'), + ) + + TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + ) + + ROOT_URLCONF = 'urls' + + WSGI_APPLICATION = 'wsgi.application' + + # Database + # https://docs.djangoproject.com/en/1.7/ref/settings/#databases + + DATABASES = values.DatabaseURLValue('sqlite:///%s' % join(BASE_DIR, 'db.sqlite3')) + + # Internationalization + # https://docs.djangoproject.com/en/1.7/topics/i18n/ + + LANGUAGE_CODE = 'en-us' + + TIME_ZONE = 'UTC' + + USE_I18N = True + + USE_L10N = True + + USE_TZ = True + + # EMAIL CONFIGURATION + EMAIL_BACKEND = values.Value('django.core.mail.backends.smtp.EmailBackend') + DEFAULT_FROM_EMAIL = values.Value('Sensor API ') + # END EMAIL CONFIGURATION + + # Static files (CSS, JavaScript, Images) + # https://docs.djangoproject.com/en/1.7/howto/static-files/ + STATIC_ROOT = join(os.path.dirname(BASE_DIR), 'staticfiles') + STATIC_URL = '/static/' + + # Django CORS + + CORS_ORIGIN_ALLOW_ALL = values.BooleanValue(True) + CORS_ALLOW_METHODS = ( + 'GET', + 'POST', + 'PUT', + 'PATCH', + 'OPTIONS' + ) + + CORS_ALLOW_HEADERS = ( + 'x-requested-with', + 'content-type', + 'accept', + 'origin', + 'authorization', + 'x-csrftoken', + 'accept-encoding' + ) + # End Django CORS + + REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.BasicAuthentication', + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.TokenAuthentication' + ), + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 100 + } diff --git a/sensor_rest_api/config/local.py b/sensor_rest_api/config/local.py new file mode 100644 index 0000000..7a41848 --- /dev/null +++ b/sensor_rest_api/config/local.py @@ -0,0 +1,14 @@ +from configurations import values +from .base import Base + + +class Local(Base): + + # DEBUG + DEBUG = values.BooleanValue(True) + TEMPLATE_DEBUG = DEBUG + # END DEBUG + + # Mail settings + EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' + # End mail settings diff --git a/sensor_rest_api/config/production.py b/sensor_rest_api/config/production.py new file mode 100644 index 0000000..b0a5cf3 --- /dev/null +++ b/sensor_rest_api/config/production.py @@ -0,0 +1,22 @@ +from configurations import values + +from .base import Base + + +class Production(Base): + + INSTALLED_APPS = Base.INSTALLED_APPS + + SECRET_KEY = values.SecretValue() + + INSTALLED_APPS += ("gunicorn", ) + + # EMAIL + EMAIL_HOST = values.Value('smtp.sendgrid.com') + EMAIL_HOST_PASSWORD = values.SecretValue(environ_prefix="", environ_name="SENDGRID_PASSWORD") + EMAIL_HOST_USER = values.SecretValue(environ_prefix="", environ_name="SENDGRID_USERNAME") + EMAIL_PORT = values.IntegerValue(587, environ_prefix="", environ_name="EMAIL_PORT") + EMAIL_SUBJECT_PREFIX = values.Value('[Sensor API] ', environ_name="EMAIL_SUBJECT_PREFIX") + EMAIL_USE_TLS = True + SERVER_EMAIL = EMAIL_HOST_USER + # END EMAIL diff --git a/sensor_rest_api/manage.py b/sensor_rest_api/manage.py new file mode 100755 index 0000000..e89d5af --- /dev/null +++ b/sensor_rest_api/manage.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +import os +import sys +import re + + +def read_env(): + """Pulled from Honcho code with minor updates, reads local default + environment variables from a .env file located in the project root + directory. + """ + try: + with open('.env') as f: + content = f.read() + except IOError: + content = '' + + for line in content.splitlines(): + m1 = re.match(r'\A([A-Za-z_0-9]+)=(.*)\Z', line) + if m1: + key, val = m1.group(1), m1.group(2) + m2 = re.match(r"\A'(.*)'\Z", val) + if m2: + val = m2.group(1) + m3 = re.match(r'\A"(.*)"\Z', val) + if m3: + val = re.sub(r'\\(.)', r'\1', m3.group(1)) + os.environ.setdefault(key, val) + +if __name__ == "__main__": + read_env() + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config") + os.environ.setdefault("DJANGO_CONFIGURATION", "Local") + + from configurations.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/sensor_rest_api/sensors/__init__.py b/sensor_rest_api/sensors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sensor_rest_api/sensors/migrations/0001_initial.py b/sensor_rest_api/sensors/migrations/0001_initial.py new file mode 100644 index 0000000..9ffbe1e --- /dev/null +++ b/sensor_rest_api/sensors/migrations/0001_initial.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Reading', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('pm10', models.IntegerField(default=0, null=True, blank=True)), + ('pm25', models.IntegerField(default=0, null=True, blank=True)), + ('pm10count', models.IntegerField(default=0, null=True, blank=True)), + ('pm25count', models.IntegerField(default=0, null=True, blank=True)), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Sensor', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('sensor_name', models.CharField(max_length=250, verbose_name=b'Sensor Name')), + ('lat', models.FloatField(null=True, verbose_name=b'Latitude', blank=True)), + ('lon', models.FloatField(null=True, verbose_name=b'Latitude', blank=True)), + ('address', models.TextField(max_length=250, null=True, verbose_name=b'Sensor Address', blank=True)), + ('serial', models.CharField(max_length=250, null=True, verbose_name=b'Serial Number', blank=True)), + ('description', models.TextField(max_length=250, null=True, verbose_name=b'Description', blank=True)), + ('account', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.AddField( + model_name='reading', + name='sensor', + field=models.ForeignKey(to='sensors.Sensor'), + preserve_default=True, + ), + ] diff --git a/sensor_rest_api/sensors/migrations/0002_sensorverification.py b/sensor_rest_api/sensors/migrations/0002_sensorverification.py new file mode 100644 index 0000000..a94776e --- /dev/null +++ b/sensor_rest_api/sensors/migrations/0002_sensorverification.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('sensors', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='SensorVerification', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('verification_code', models.CharField(max_length=250, verbose_name=b'Verification Code')), + ('updated', models.DateTimeField(auto_now=True)), + ('verified', models.BooleanField(default=False)), + ('account', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + options={ + }, + bases=(models.Model,), + ), + ] diff --git a/sensor_rest_api/sensors/migrations/0003_reading_hour_code.py b/sensor_rest_api/sensors/migrations/0003_reading_hour_code.py new file mode 100644 index 0000000..f9c064f --- /dev/null +++ b/sensor_rest_api/sensors/migrations/0003_reading_hour_code.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('sensors', '0002_sensorverification'), + ] + + operations = [ + migrations.AddField( + model_name='reading', + name='hour_code', + field=models.CharField(max_length=100, null=True, verbose_name=b'Hour Code', blank=True), + preserve_default=True, + ), + ] diff --git a/sensor_rest_api/sensors/migrations/__init__.py b/sensor_rest_api/sensors/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sensor_rest_api/sensors/models.py b/sensor_rest_api/sensors/models.py new file mode 100644 index 0000000..6fde3af --- /dev/null +++ b/sensor_rest_api/sensors/models.py @@ -0,0 +1,51 @@ +from django.db import models +from django.contrib.auth.models import User +from django.dispatch import receiver +from django.db.models.signals import post_save + + +class Sensor(models.Model): + + account = models.ForeignKey(User) + sensor_name = models.CharField('Sensor Name', max_length=250) + lat = models.FloatField('Latitude', null=True, blank=True) + lon = models.FloatField('Latitude', null=True, blank=True) + address = models.TextField('Sensor Address', max_length=250, null=True, blank=True) + serial = models.CharField('Serial Number', max_length=250, null=True, blank=True) + description = models.TextField('Description', max_length=250, null=True, blank=True) + + def __unicode__(self): + return self.sensor_name + + +class Reading(models.Model): + sensor = models.ForeignKey(Sensor) + created = models.DateTimeField(auto_now_add=True) + hour_code = models.CharField('Hour Code', null=True, blank=True, max_length=100) + pm10 = models.IntegerField(default=0, null=True, blank=True) + pm25 = models.IntegerField(default=0, null=True, blank=True) + pm10count = models.IntegerField(default=0, null=True, blank=True) + pm25count = models.IntegerField(default=0, null=True, blank=True) + + def __unicode__(self): + return '%s: %s' % (self.sensor, self.created) + + +class SensorVerification(models.Model): + + account = models.ForeignKey(User) + verification_code = models.CharField('Verification Code', max_length=250) + updated = models.DateTimeField(auto_now=True) + verified = models.BooleanField(default=False) + + def __unicode__(self): + return '%s: %s' % (self.account.email, self.verified) + + +@receiver(post_save, sender=Reading) +def add_hour_code(sender, instance, created, raw, using, update_fields, **kwargs): + """ Generate a string from datetime and add to hour_code based on the created time """ + + if not instance.hour_code: + instance.hour_code = instance.created.strftime('%Y%m%d%H') + instance.save() diff --git a/sensor_rest_api/sensors/templates/sensors/base.html b/sensor_rest_api/sensors/templates/sensors/base.html new file mode 100644 index 0000000..bee9f0f --- /dev/null +++ b/sensor_rest_api/sensors/templates/sensors/base.html @@ -0,0 +1,15 @@ + + + + + + + + Verify the Sensor + + + + + {% block content %}{% endblock %} + + diff --git a/sensor_rest_api/sensors/templates/sensors/error.html b/sensor_rest_api/sensors/templates/sensors/error.html new file mode 100644 index 0000000..7f1ce56 --- /dev/null +++ b/sensor_rest_api/sensors/templates/sensors/error.html @@ -0,0 +1,11 @@ +{% extends 'sensors/base.html' %} + +{% block content %} + +
+ +

{{message}}

+
+{% endblock %} diff --git a/sensor_rest_api/sensors/templates/sensors/verify.html b/sensor_rest_api/sensors/templates/sensors/verify.html new file mode 100644 index 0000000..a20d875 --- /dev/null +++ b/sensor_rest_api/sensors/templates/sensors/verify.html @@ -0,0 +1,13 @@ +{% extends 'sensors/base.html' %} + +{% block content %} + +
+ +

Your API Token is {{token}}

+

You can start using the sensor.

+

We also just emailed you the API token.

+
+{% endblock %} diff --git a/sensor_rest_api/sensors/urls.py b/sensor_rest_api/sensors/urls.py new file mode 100644 index 0000000..b91af5a --- /dev/null +++ b/sensor_rest_api/sensors/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url + +from sensors import views + +urlpatterns = [ + url(r'^$', views.verify, name='verify') +] diff --git a/sensor_rest_api/sensors/views.py b/sensor_rest_api/sensors/views.py new file mode 100644 index 0000000..c929c11 --- /dev/null +++ b/sensor_rest_api/sensors/views.py @@ -0,0 +1,56 @@ +from django.shortcuts import render +from django.db import transaction +from rest_framework.authtoken.models import Token + +from sensors.models import SensorVerification, Sensor +from api.v1.mailer import TokenEmail + + +def verify(request): + + code = request.GET.get('code') + + print code + + if code is None: + return render(request, 'sensors/error.html', + {'message': 'The page requested was not found'}) + else: + try: + verify = SensorVerification.objects.get(verification_code=code) + + if verify.verified: + return render( + request, + 'sensors/error.html', + { + 'message': 'This verification code is already used. If you look for the API token, ' + + 'check your email.' + } + ) + else: + with transaction.atomic(): + verify.verified = True + verify.save() + # Activate user account + verify.account.is_active = True + verify.account.save() + + # Create token + token = Token.objects.get_or_create(user=verify.account) + + # Send Token Email + sensor = Sensor.objects.get(account=verify.account) + + mail = TokenEmail(verify.account.email, token[0].key, sensor.sensor_name) + mail.send() + + return render(request, 'sensors/verify.html', {'verify': verify, + 'token': token[0].key, + 'sensor': sensor}) + + except SensorVerification.DoesNotExist: + return render(request, 'sensors/error.html', + {'message': 'The verification code is invalid'}) + + diff --git a/sensor_rest_api/templates/rest_framework/base.html b/sensor_rest_api/templates/rest_framework/base.html new file mode 100644 index 0000000..d7cc4d8 --- /dev/null +++ b/sensor_rest_api/templates/rest_framework/base.html @@ -0,0 +1,257 @@ +{% load url from future %} +{% load staticfiles %} +{% load rest_framework %} + + + + {% block head %} + + {% block meta %} + + + {% endblock %} + + {% block title %}Django REST framework{% endblock %} + + {% block style %} + {% block bootstrap_theme %} + + {% endblock %} + {% endblock %} + + {% endblock %} + + + {% block body %} + + +
+ + {% block navbar %} + + {% endblock %} + +
+ {% block breadcrumbs %} + + {% endblock %} + + +
+ + {% if 'GET' in allowed_methods %} +
+
+
+ GET + + + +
+
+
+ {% endif %} + + {% if options_form %} +
+ {% csrf_token %} + + +
+ {% endif %} + + {% if delete_form %} +
+ {% csrf_token %} + + +
+ {% endif %} + +
+ +
+ {% block description %} + {{ description }} + {% endblock %} +
+ + {% if paginator %} + + {% endif %} + +
+
{{ request.method }} {{ request.get_full_path }}
+
+
+
HTTP {{ response.status_code }} {{ response.status_text }}{% autoescape off %}
+{% for key, val in response_headers.items %}{{ key }}: {{ val|break_long_headers|urlize_quoted_links }}
+{% endfor %}
+{{ content|urlize_quoted_links }}
{% endautoescape %} +
+
+ + {% if display_edit_forms %} + + {% if post_form or raw_data_post_form %} +
+ {% if post_form %} + + {% endif %} +
+ {% if post_form %} +
+ {% with form=post_form %} +
+
+ {{ post_form }} +
+ +
+
+
+ {% endwith %} +
+ {% endif %} +
+ {% with form=raw_data_post_form %} +
+
+ {% include "rest_framework/raw_data_form.html" %} +
+ +
+
+
+ {% endwith %} +
+
+
+ {% endif %} + + {% if put_form or raw_data_put_form or raw_data_patch_form %} +
+ {% if put_form %} + + {% endif %} +
+ {% if put_form %} +
+
+
+ {{ put_form }} +
+ +
+
+
+
+ {% endif %} +
+ {% with form=raw_data_put_or_patch_form %} +
+
+ {% include "rest_framework/raw_data_form.html" %} +
+ {% if raw_data_put_form %} + + {% endif %} + {% if raw_data_patch_form %} + + {% endif %} +
+
+
+ {% endwith %} +
+
+
+ {% endif %} + {% endif %} +
+ +
+
+ + {% block script %} + + + {% endblock %} + + {% endblock %} + diff --git a/sensor_rest_api/urls.py b/sensor_rest_api/urls.py new file mode 100644 index 0000000..32239a7 --- /dev/null +++ b/sensor_rest_api/urls.py @@ -0,0 +1,9 @@ +from django.conf.urls import patterns, include, url +from django.views.generic.base import RedirectView +from django.conf import settings + +urlpatterns = patterns('', + url(r'^$', RedirectView.as_view(url=settings.DOCS_URL)), + url(r'^api/v1/', include('api.v1.urls')), + url(r'^verify/', include('sensors.urls')), +) diff --git a/sensor_rest_api/wsgi.py b/sensor_rest_api/wsgi.py new file mode 100644 index 0000000..7bee8f6 --- /dev/null +++ b/sensor_rest_api/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for sensor project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ +""" + +import os + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config") +os.environ.setdefault("DJANGO_CONFIGURATION", "Production") + +from configurations.wsgi import get_wsgi_application +application = get_wsgi_application() From 7782efe6a49d635faf766f6cbe65dd24d5591cde Mon Sep 17 00:00:00 2001 From: smit1678 Date: Sat, 30 May 2015 19:59:04 +0100 Subject: [PATCH 5/9] add post deploy script and noinput --- app.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app.json b/app.json index cb93d8c..7d348fc 100644 --- a/app.json +++ b/app.json @@ -13,5 +13,8 @@ "required": true, "generator": "secret" } + }, + "scripts": { + "postdeploy": "python sensor_rest_api/manage.py syncdb --noinput" } } From 0d3a7bb51dfce5a987e50bd07abc2cad9e17acab Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Sat, 30 May 2015 20:34:31 +0100 Subject: [PATCH 6/9] add noinput flag --- app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.json b/app.json index 8ed0954..ad316c3 100644 --- a/app.json +++ b/app.json @@ -15,6 +15,6 @@ } }, "scripts": { - "postdeploy": "python sensor_rest_api/manage.py syncdb" + "postdeploy": "python sensor_rest_api/manage.py syncdb --noinput" } } From 53ee0ec0ef20214597807bf5ee6b894c9da1ecee Mon Sep 17 00:00:00 2001 From: Scisco Date: Thu, 4 Jun 2015 10:23:07 -0400 Subject: [PATCH 7/9] add allow host --- sensor_rest_api/config/production.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sensor_rest_api/config/production.py b/sensor_rest_api/config/production.py index b0a5cf3..31a0862 100644 --- a/sensor_rest_api/config/production.py +++ b/sensor_rest_api/config/production.py @@ -10,6 +10,7 @@ class Production(Base): SECRET_KEY = values.SecretValue() INSTALLED_APPS += ("gunicorn", ) + ALLOWED_HOSTS = ['*'] # EMAIL EMAIL_HOST = values.Value('smtp.sendgrid.com') From b0e5be163a188dd726eda59676ba6833d674d9f0 Mon Sep 17 00:00:00 2001 From: Scisco Date: Thu, 4 Jun 2015 10:25:06 -0400 Subject: [PATCH 8/9] update documentation url --- sensor_rest_api/config/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sensor_rest_api/config/base.py b/sensor_rest_api/config/base.py index 1200172..8273859 100644 --- a/sensor_rest_api/config/base.py +++ b/sensor_rest_api/config/base.py @@ -22,7 +22,7 @@ class Base(Configuration): BASE_DIR = BASE_DIR PORTAL_URL = values.Value('http://127.0.0.1:8000') - DOCS_URL = values.Value('http://docs.sensorrestapi.apiary.io/#reference') + DOCS_URL = values.Value('https://developmentseed.org/dustduino-server/') INSTALLED_APPS = ( 'django.contrib.admin', From f2bd185f24fed23d265785f4f50c914a4c219065 Mon Sep 17 00:00:00 2001 From: smit1678 Date: Fri, 12 Jun 2015 09:46:28 -0400 Subject: [PATCH 9/9] tweak travis, add to readme, remove secure key --- .travis.yml | 5 ----- README.md | 8 +++++++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index dfd8e16..4048b6e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,11 +6,6 @@ branches: only: - master -env: - global: - - GH_REF=github.com/developmentseed/dustduino-server.git - - secure: CbzgND/x3QGeJmVIpLpjuHMn9SFuRkpyWL65KjLanM7k+h86JfmeYaOxnL/6yXYeh6klzYOq8lBQQZayfC9Pre0acrucNB0vyRBLfoW7zeUWeIuQ9Nz1JpjDNNtovDz19mpP8JxzsvzoWdIsfvT8hfeN1/P2a5V+lzZGn50c/9I= - install: echo 'no install' script: echo 'no scripts' diff --git a/README.md b/README.md index 9d6c4da..cf8b5b9 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ A REST API for DustDuino air quality sensors [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) -For API documentation [click here](http://devseed.com/dustduino-server/). +For API documentation [click here](http://api.opendustmap.com). ## Deployment to Heroku @@ -36,3 +36,9 @@ Start the server ``` python sensor_rest_api/manage.py runserver ``` +## Docs Deployment + +This project is set up with [Travis](https://travis-ci.org/) deploy scripts already included which will deploy documentation to `gh-pages`. To utilize these, you will need to enable the repo on the Travis system and include the following two environment variables. + +- **GH_REF** - the URL of the repo, similar to `github.com/developmentseed/dustduino-server.git` +- **GH_TOKEN** - a GitHub personal access token available from https://github.com/settings/tokens \ No newline at end of file