Skip to content

Commit

Permalink
API cleanup - adding last_metering
Browse files Browse the repository at this point in the history
* added CACHES to settings
* added Station.last_metering with cache
* fix of FuzzyFloat in factories, created FuzzyFloadRound
* some other minior factories fixes
* added tests for StationAPI
* `Station.last_metering` is cached until HW station do not upload new metering throught `station-detail/add_metering/` endpoint
* fixing @khasinski suggestions
* reversing order -  cache invalidation + Metering creation in add_metering
  • Loading branch information
lechup committed Feb 21, 2017
1 parent 4b73ad4 commit 97b566a
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 30 deletions.
10 changes: 10 additions & 0 deletions EnviroMonitorWeb/settings/base.py
Expand Up @@ -36,6 +36,7 @@
'django.contrib.gis',
'rest_framework',
'rest_framework_swagger',
'rest_framework_gis',
'crispy_forms',
'django_filters',
'django_extensions',
Expand Down Expand Up @@ -130,6 +131,15 @@

MEDIA_ROOT = os.path.join(BASE_DIR, "media")

# cache
# https://docs.djangoproject.com/en/1.10/topics/cache/#local-memory-caching

CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'smogly'
}
}
# User settings

# Provide your API key to Google Maps
Expand Down
7 changes: 7 additions & 0 deletions EnviroMonitorWeb/settings/test.py
@@ -1,3 +1,10 @@
from .base import *

ALLOWED_HOSTS = ['*']

CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'smogly-test'
}
}
7 changes: 7 additions & 0 deletions api/exceptions.py
@@ -0,0 +1,7 @@
from rest_framework.exceptions import APIException


class StationWrongToken(APIException):
status_code = 403
default_detail = 'Wrong token, please provide proper token parameter.'
default_code = 'station_wrong_token'
19 changes: 18 additions & 1 deletion api/models.py
@@ -1,5 +1,6 @@
import uuid
from django.conf import settings
from django.core.cache import cache
from django.contrib.gis.db import models as gis_models
from django.core.urlresolvers import reverse
from django.db import models
Expand Down Expand Up @@ -142,6 +143,22 @@ class Station(AbstractTimeTrackable, AbstractLocation):
owner = models.ForeignKey(settings.AUTH_USER_MODEL)
project = models.ForeignKey('api.Project')

@property
def last_metering(self):
"""
Return lastly created, serialized Metering object.
We remove cache key while adding metering to given station.
"""
if not cache.get(self.last_metering_cache_key):
from api.serializers import MeteringSerializer
last_metering = self.metering_set.first()
cache.set(self.last_metering_cache_key, MeteringSerializer(last_metering).data)
return cache.get(self.last_metering_cache_key)

@property
def last_metering_cache_key(self):
return u'station-{}-last-metering'.format(self.pk)

class Meta:
ordering = ('-created',)

Expand Down Expand Up @@ -198,7 +215,7 @@ class Project(AbstractTimeTrackable, AbstractLocation):

id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=255)
slug = AutoSlugField(populate_from='name', blank=True)
slug = AutoSlugField(populate_from='name', blank=True, unique=True)

website = models.URLField()
description = models.TextField()
Expand Down
12 changes: 10 additions & 2 deletions api/serializers.py
Expand Up @@ -25,7 +25,8 @@ class Meta:
'city',
'district',
'owner',
'project'
'project',
'last_metering',
)


Expand Down Expand Up @@ -68,5 +69,12 @@ class Meta:
'website',
'description',
'logo',
'owner'
'position',
'country',
'state',
'county',
'community',
'city',
'district',
'owner',
)
46 changes: 30 additions & 16 deletions api/tests/factories.py
Expand Up @@ -13,6 +13,21 @@
from api.models import Station, Metering, MeteringHistory, Project


class FuzzyFloatRound(factory.fuzzy.FuzzyFloat):
"""Random float within a given range with ndigits option, that will round fuzzer to ndigits."""

def __init__(self, *args, **kwargs):
self.ndigits = kwargs.pop('ndigits')

super(FuzzyFloatRound, self).__init__(*args, **kwargs)

def fuzz(self):
fuzz = super(FuzzyFloatRound, self).fuzz()
if self.ndigits:
return round(fuzz, self.ndigits)
return fuzz


class AbstractLocationFactory(factory.django.DjangoModelFactory):

@factory.lazy_attribute
Expand Down Expand Up @@ -93,14 +108,13 @@ class StationFactory(AbstractLocationFactory):
name = factory.Sequence(lambda n: 'Smogly Station %04d' % n)
type = factory.fuzzy.FuzzyChoice([type_choice[0] for type_choice in Station.TYPE_CHOICES])
notes = factory.Faker('sentences', nb=3)
altitude = factory.fuzzy.FuzzyFloat(0.0, 40.0)
altitude = FuzzyFloatRound(0.0, 300.0, ndigits=2)
project = factory.SubFactory(ProjectFactory, **{
'position': factory.SelfAttribute('position'),
'country': factory.SelfAttribute('country'),
'state': factory.SelfAttribute('state'),
'county': factory.SelfAttribute('county'),
'community': factory.SelfAttribute('community'),
'district': factory.SelfAttribute('district')
'country': factory.SelfAttribute('..country'),
'state': factory.SelfAttribute('..state'),
'county': factory.SelfAttribute('..county'),
'community': factory.SelfAttribute('..community'),
'district': factory.SelfAttribute('..district')
})
owner = factory.SubFactory(UserFactory)

Expand All @@ -109,19 +123,19 @@ class Meta:


class AbstractMeteringFactory(factory.django.DjangoModelFactory):
pm01 = factory.fuzzy.FuzzyFloat(0.0, 150.0)
pm25 = factory.fuzzy.FuzzyFloat(0.0, 150.0)
pm10 = factory.fuzzy.FuzzyFloat(0.0, 150.0)
temp_out1 = factory.fuzzy.FuzzyFloat(-25.0, 30.0)
pm01 = FuzzyFloatRound(0.0, 150.0, ndigits=2)
pm25 = FuzzyFloatRound(0.0, 150.0, ndigits=2)
pm10 = FuzzyFloatRound(0.0, 150.0, ndigits=2)
temp_out1 = FuzzyFloatRound(-25.0, 30.0, ndigits=2)
temp_out2 = factory.SelfAttribute('temp_out1')
temp_out3 = factory.SelfAttribute('temp_out1')
temp_int_air1 = factory.fuzzy.FuzzyFloat(28.0, 30.0)
hum_out1 = factory.fuzzy.FuzzyFloat(5.0, 99.0)
temp_int_air1 = FuzzyFloatRound(28.0, 30.0, ndigits=2)
hum_out1 = FuzzyFloatRound(5.0, 99.0, ndigits=2)
hum_out2 = factory.SelfAttribute('hum_out1')
hum_out3 = factory.SelfAttribute('hum_out1')
hum_int_air1 = factory.fuzzy.FuzzyFloat(30.0, 35.0)
rssi = factory.fuzzy.FuzzyFloat(-100.0, 0.0)
bpress_out1 = factory.fuzzy.FuzzyFloat(900, 1100)
hum_int_air1 = FuzzyFloatRound(30.0, 35.0, ndigits=2)
rssi = FuzzyFloatRound(-100.0, 0.0, ndigits=2)
bpress_out1 = factory.fuzzy.FuzzyInteger(900, 1100)

station = factory.SubFactory(StationFactory)

Expand Down
120 changes: 110 additions & 10 deletions api/tests/test_views.py
Expand Up @@ -10,10 +10,13 @@

from .factories import (
Project,
Station,
ProjectFactory,
UserFactory
UserFactory,
StationFactory,
MeteringFactory
)
from ..serializers import ProjectSerializer
from ..serializers import ProjectSerializer, StationSerializer, MeteringSerializer


class ProjectApiTests(APITestCase):
Expand All @@ -27,6 +30,19 @@ def create_project(self):
self.client.login(username=self.user.username, password=UserFactory.DEFAULT_PASSWORD)
return self.client.post(self.project_list_url, self.project_data, format='json')

def assertProjectDataEqual(self, data):
self.assertEqual(data['name'], self.project_data['name'])
self.assertEqual(data['website'], self.project_data['website'])
self.assertEqual(data['description'], self.project_data['description'])
self.assertEqual(data['logo'], self.project_data['logo'])
self.assertEqual(data['position'], self.project_data['position'])
self.assertEqual(data['country'], self.project_data['country'])
self.assertEqual(data['state'], self.project_data['state'])
self.assertEqual(data['county'], self.project_data['county'])
self.assertEqual(data['community'], self.project_data['community'])
self.assertEqual(data['city'], self.project_data['city'])
self.assertEqual(data['district'], self.project_data['district'])

def test_project_create(self):
self.assertEqual(Project.objects.count(), 0)
api_response = self.create_project()
Expand All @@ -35,10 +51,7 @@ def test_project_create(self):

created_project = Project.objects.get()
created_project_data = ProjectSerializer(created_project).data
self.assertEqual(created_project_data['name'], self.project_data['name'])
self.assertEqual(created_project_data['website'], self.project_data['website'])
self.assertEqual(created_project_data['description'], self.project_data['description'])
self.assertEqual(created_project_data['logo'], self.project_data['logo'])
self.assertProjectDataEqual(created_project_data)
self.assertEqual(created_project.owner, self.user)

def test_project_create_anon(self):
Expand All @@ -57,10 +70,7 @@ def test_project_detail(self):
format='json'
)
self.assertEqual(api_response.status_code, HTTP_200_OK)
self.assertEqual(api_response.data['name'], self.project_data['name'])
self.assertEqual(api_response.data['website'], self.project_data['website'])
self.assertEqual(api_response.data['description'], self.project_data['description'])
self.assertEqual(api_response.data['logo'], self.project_data['logo'])
self.assertProjectDataEqual(api_response.data)

def test_project_detail_id_does_not_exist(self):
api_response = self.client.get(
Expand Down Expand Up @@ -89,3 +99,93 @@ def test_project_detail_delete(self):
)
self.assertEqual(api_response.status_code, HTTP_204_NO_CONTENT)
self.assertEqual(Project.objects.count(), 0)


class StationApiTests(APITestCase):
def setUp(self):
self.user = UserFactory()
self.existing_project = ProjectFactory.create()
self.station = StationFactory.build(project=self.existing_project)
self.station_data = StationSerializer(self.station).data
self.station_list_url = reverse('station-list')

def create_station(self):
self.client.login(username=self.user.username, password=UserFactory.DEFAULT_PASSWORD)
return self.client.post(self.station_list_url, self.station_data, format='json')

def assertStationDataEqual(self, data):
self.assertEqual(data['name'], self.station_data['name'])
self.assertEqual(data['type'], self.station_data['type'])
self.assertEqual(data['notes'], self.station_data['notes'])
self.assertEqual(data['is_in_test_mode'], self.station_data['is_in_test_mode'])
self.assertEqual(data['altitude'], self.station_data['altitude'])
self.assertEqual(data['position'], self.station_data['position'])
self.assertEqual(data['country'], self.station_data['country'])
self.assertEqual(data['state'], self.station_data['state'])
self.assertEqual(data['county'], self.station_data['county'])
self.assertEqual(data['community'], self.station_data['community'])
self.assertEqual(data['city'], self.station_data['city'])
self.assertEqual(data['district'], self.station_data['district'])
self.assertEqual(data['project'], self.station_data['project'])
self.assertEqual(data['last_metering'], self.station_data['last_metering'])

def test_station_create(self):
self.assertEqual(Station.objects.count(), 0)
api_response = self.create_station()
self.assertEqual(api_response.status_code, HTTP_201_CREATED)
self.assertEqual(Station.objects.count(), 1)

created_station = Station.objects.get()
created_station_data = StationSerializer(created_station).data
self.assertStationDataEqual(created_station_data)
self.assertEqual(created_station.owner, self.user)

def test_add_metering(self):
station = StationFactory.create()

metering = MeteringFactory.build(station=None)
metering_data = MeteringSerializer(metering).data

# test cache key removal before add_metering
self.assertEqual(station.last_metering, MeteringSerializer(None).data)

self.assertEqual(station.metering_set.count(), 0)
add_metering_api_url = '{}?token={}'.format(
reverse('station-add-metering', kwargs={'pk': station.pk}),
station.token
)
api_response = self.client.post(add_metering_api_url, metering_data, format='json')
self.assertEqual(api_response.status_code, 200)
self.assertEqual(station.metering_set.count(), 1)

# test cache key removal after add_metering
self.assertEqual(station.last_metering, MeteringSerializer(station.metering_set.first()).data)

def test_add_metering_no_token(self):
station = StationFactory.create()

add_metering_api_url = '{}'.format(
reverse('station-add-metering', kwargs={'pk': station.pk}),
)
api_response = self.client.post(add_metering_api_url, {}, format='json')
self.assertEqual(api_response.status_code, 403)

def test_add_metering_wrong_token(self):
station = StationFactory.create()

add_metering_api_url = '{}?token={}'.format(
reverse('station-add-metering', kwargs={'pk': station.pk}),
'xyz'
)
api_response = self.client.post(add_metering_api_url, {}, format='json')
self.assertEqual(api_response.status_code, 403)

def test_add_metering_wrong_data(self):
station = StationFactory.create()

add_metering_api_url = '{}?token={}'.format(
reverse('station-add-metering', kwargs={'pk': station.pk}),
station.token
)
api_response = self.client.post(add_metering_api_url, {}, format='json')
self.assertEqual(api_response.status_code, 400)
27 changes: 27 additions & 0 deletions api/views.py
@@ -1,11 +1,16 @@
from django.core.cache import cache
from rest_framework.permissions import AllowAny
from rest_framework.status import HTTP_400_BAD_REQUEST
from rest_framework.viewsets import ModelViewSet
from rest_framework.decorators import api_view, renderer_classes
from rest_framework import response, schemas
from rest_framework_swagger.renderers import OpenAPIRenderer, SwaggerUIRenderer
from rest_framework.decorators import detail_route

from .models import Station, Metering, Project
from .serializers import StationSerializer, MeteringSerializer, ProjectSerializer
from .filters import StationFilterSet, MeteringFilterSet, ProjectFilterSet
from .exceptions import StationWrongToken


class StationViewSet(ModelViewSet):
Expand All @@ -15,6 +20,28 @@ class StationViewSet(ModelViewSet):
serializer_class = StationSerializer
filter_class = StationFilterSet

@detail_route(methods=['post'], permission_classes=[AllowAny], url_path='add-metering')
def add_metering(self, request, pk=None):
station = self.get_object()
request_token = request.query_params.get('token')
if request_token is None or station.token != request_token:
raise StationWrongToken

metering_serializer = MeteringSerializer(data=request.data)
if metering_serializer.is_valid():
# create Metering from selected station and provided data
Metering.objects.create(station=station, **metering_serializer.data)
# remove last_metering cache key
cache.delete(station.last_metering_cache_key)
return response.Response({
'status': 'metering added'
})

return response.Response(
metering_serializer.errors,
status=HTTP_400_BAD_REQUEST
)


class MeteringViewSet(ModelViewSet):
"""ViewSet for the Metering class"""
Expand Down

0 comments on commit 97b566a

Please sign in to comment.