Adds App, commit fixup

- Creates APIs to CRUD providers and service areas
- Create an API to return service areas given latitude and longitude
- Adds docker, docker compose and setup App v1.0
- Adds cache and heuristic
master
nicolas 3 years ago
parent e5d62831f9
commit f433e8b552

4
.gitignore vendored

@ -0,0 +1,4 @@
**/*.pyc
**/terraform/
update-ecs.py

@ -0,0 +1,38 @@
FROM python:3.10.4-alpine3.15
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE 1
COPY . /app
RUN apk update \
&& apk add --virtual build-deps gcc musl-dev \
&& apk add --no-cache \
postgresql-dev \
libffi-dev \
py-cffi \
gettext \
postgresql-client \
libpq-dev \
py3-gunicorn \
&& apk add --no-cache --virtual .build-deps-edge --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing \
gdal-dev \
geos-dev \
proj-dev \
&& pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir virtualenv \
&& virtualenv /env \
&& /env/bin/pip install --no-cache-dir --upgrade pip \
&& /env/bin/pip install -r /app/requirements-dev.txt \
&& rm -rf /root/.cache
RUN adduser -D -H backoffice_user \
&& mkdir -p /app/log \
&& touch /app/log/shuttleapp.log \
&& chown -R backoffice_user:backoffice_user /app \
&& chmod +x /app/docker/entrypoint.sh
EXPOSE 8091
CMD ["/app/docker/entrypoint.sh"]

@ -0,0 +1,26 @@
SHELL := /bin/bash
all: build test
build:
docker-compose build
up:
docker-compose up -d
down:
docker-compose down --remove-orphans
shell:
docker-compose run shuttleapp /env/bin/python /app/manage.py shell
test: up
docker-compose exec shuttleapp /env/bin/python /app/manage.py test
migrate: up
docker-compose exec shuttleapp /env/bin/python /app/manage.py migrate
makemigrations: up
docker-compose exec shuttleapp /env/bin/python /app/manage.py makemigrations
.PHONY: build up test

@ -0,0 +1,353 @@
# Shuttle App
This service is created to help transportation suppliers to define their own "service area" in a map, giving them flexibility and removing the limitation of ZIP codes or city boundaries.
This document serves as a technical specification and documentation of the project.
## Requirements
- REST API to CRUD Providers.
- REST API to CRUD Service Areas.
- An API to retrieve the service areas covering a specific point defined by its geo-coordinates.
## Solution Description
The solution will be implemented in Python, using the Django framework with the library django-rest-framework to build REST APIs.
The application will run inside a Docker container and will be deployed to AWS.
The database will be PostgreSQL and there will be geographical information system tools and libraries used.
There will be a docker-compose file managed with a Makefile, to build the container, run test cases and to provide helpers for development.
There will be a cache implemented using Redis, and heuristics to ensure searches will run fast.
## Technical Specifications
The solution will be contained in a package called **providers**.
### Models
There are two models, Provider and ServiceArea. This design allows one provider to have zero, one, or many service areas:
![UML diagram](uml.png?raw=true "UML")
**Provider**
```sh
currency - string (ISO 4217 format)
email - string
language - string (ISO 639-3 format)
name - string
phone_number - string
timestamp - datetime (auto field)
```
**ServiceArea**
```sh
name - string
price - float
polygon - Polygon
provider - Foreign Key(Provider)
```
The Polygon type is a list of tuples. Each tuple defines a valid point, and the list contains a valid Polygon, meaning that each point connects with the next one, and the last point closes the polygon by connecting to the first one. A valid polygon example is:
```python
[
[0.0, 0.0],
[0.0, 50.0],
[50.0, 50.0],
[50.0, 0.0],
[0.0, 0.0]
]
```
**WorldArea**
```sh
code - string
polygon - Polygon
```
This table is populated at database creation time with a data migrations. It's intended to be used as a tool for the heuristic implemented for the cache. For more details refer to the cache section.
### APIs
#### **POST /provider**
Creates a Provider in database.
**Input payload:**
```javascript
{
'currency': str,
'email': str,
'language': str,
'name': str,
'phone_number': str
}
```
**Response:**
- 400 and an appropiate message in the case of missing fields or invalid field values. There is validation of the phone number, email, currency and language.
- 201 in the case the provider was successfully created, with the following content:
```javascript
{
'currency': str,
'email': str,
'id': int,
'language': str,
'name': str,
'phone_number': str,
'timestamp': str // datetime of creation
}
```
#### **GET /provider**
Lists all the providers in database.
**Response:**
- 200 and a list of all providers in the system:
```javascript
[
{
'currency': str,
'email': str,
'id': int,
'language': str,
'name': str,
'phone_number': str,
'timestamp': str
},
...
]
```
#### **PUT /provider/:id:**
Updates a Provider in database.
**Input payload:**
```javascript
{
'currency': str,
'email': str,
'language': str,
'name': str,
'phone_number': str
}
```
**Response:**
- 400 and an appropiate message in the case of missing fields or invalid field values. There is validation of the phone number, email, currency and language.
- 200 in the case the provider was successfully updated, with the following content:
```javascript
{
'currency': str,
'email': str,
'id': int,
'language': str,
'name': str,
'phone_number': str,
'timestamp': str
}
```
#### **GET /provider/:id:**
Retrieves a provider and its service areas from database.
**Response:**
- 404 in case the provider with the given id doesn't exist in database.
- 200 in case the provider was found, with the following content:
```javascript
{
'currency': str,
'email': str,
'id': int,
'language': str,
'name': str,
'phone_number': str,
'timestamp': str
}
```
#### **DELETE /provider/:id:**
Deletes a provider in database.
**Response:**
- 404 in case the provider with the given id doesn't exist in database.
- 204 in case the provider was successfully deleted.
#### **POST /provider/service-area**
Creates a Service Area in database.
**Input payload:**
```javascript
{
'name': str,
'price': float,
'provider': int,
'polygon': list[list[float]]
}
```
**Example:**
```javascript
{
'name': 'Bermuda Triangle',
'price': 40.5,
'provider': 1,
'polygon': [[0.0, 0.0], [25.0, 50.0], [50.0, 0.0], [0.0, 0.0]]
}
```
**Response:**
- 400 and an appropiate message in the case of missing fields or invalid field values. There is validation of the provider id and polygon.
- 201 in the case the service area was successfully created.
#### **GET /provider/service-area**
Lists all the service area objects in database.
**Response:**
- 200 and a list of all service area objects.
```javascript
[
{
'name': str,
'price': float,
'provider': int,
'polygon': list[list[float]]
},
...
]
```
#### **PUT /provider/service-area/:id:**
Updates a Service Area in database.
**Input payload:**
```javascript
{
'name': str,
'price': float,
'provider': int,
'polygon': list[list[float]]
}
```
**Response:**
- 400 and an appropiate message in the case of missing fields or invalid field values. There is validation of polygon field, and also a check if the provider id corresponds to the object provider id.
- 200 in the case the service area was successfully updated.
#### **GET /provider/service-area/:id:**
Retrieves a service area from database.
**Response:**
- 404 in case the service area with the given id doesn't exist in database.
- 200 in case the service area was found, with the following content:
```javascript
{
'name': str,
'price': float,
'provider': int,
'polygon': list[list[float]]
}
```
#### **DELETE /provider/service-area/:id:**
Deletes a service area in database.
**Response:**
- 404 in case the service area with the given id doesn't exist in database.
- 204 in case the service was successfully deleted.
#### **GET /provider/service-area/point**
Lists all the service area objects in database that contain the given geopoint.
**QueryString parameters:**
```javascript
latitude: float
longitude: float
```
Both parameters are required.
**Response:**
- 400 if there's missing parameters
- 400 if the values for latitude and longitude are not valid numbers or an invalid latitude/longitude.
- 200 and a list of all service area objects that contain the given point.
```javascript
[
{
'name': str,
'price': float,
'provider': int,
'polygon': list[list[float]]
},
...
]
```
## Cache
As the database will grow bigger the execution times will grow. To avoid this there is a Redis instance configured to work as cache.
The cache is a simple dictionary and works as follows:
- The **WorldArea** model is populated with Polygons that represent a square, covering all the world by dividing the latitude and longitude. Right now the division is one degree, making squares of 1x1 degree which is already a lot, but this can be a improvement in the future.
- Everytime a ServiceArea object is created, updated, or deleted, we search for the intersection of the ServiceArea Polygon and the WorldArea squares, obtaining all the squares where this ServiceArea is contained. We save this information in the cache.
- The cache structure is a dictionary where the key has the following format: **{{LATITUDE}}_{{LONGITUDE}}**, both parameters are truncated to integers. The value is a list of ids referencing ServiceArea objects.
- The idea of the cache is to implement an heuristic to filter the ServiceArea table as much as possible and make the searches as fast a possible when this table grows.
The cache is implemented in each endpoint as follows:
#### **DELETE /provider/:id:**
When a Provider is deleted together with all its ServiceArea objects, we don't need the cache mapping anymore. All the service areas are removed from the corresponding cache entries.
#### **POST /provider/service-area**
When a new ServiceArea object is created, the intersection with the WorldArea is calculated and the information is added to the cache.
#### **PUT /provider/service-area/:id:**
When a ServiceArea object is updated, the old information is removed from the cache and the new information is added.
#### **DELETE /provider/service-area/:id:**
When a ServiceArea is deleted from database, the information is also deleted from the cache.
#### **GET /provider/service-area/point**
Here we make use of the cache to query the ServiceArea model. We are provided with a latitude and longitude, so we can manipulate this information to generate the cache key of the WorldArea square that contains the point.
With this cache key we can quickly obtain all the ServiceArea object ids that belong to the WorldArea square containing the point.
With these ids we can filter the ServiceArea model. With the result of this query we finally filter for the ServiceArea Polygons that contain the given Point.
## Improvements
- Setup a system to populate the cache at startup. There is a function called **populate_cache** implemented for this task in *apps/parameters/actions/setup_cache.py*
- Implement a system to avoid crashes if the cache is not available, specially in the **GET /provider/service-area/point** view, where we should have a fallback system to return results without applying the heuristic.
- Remove WorldAreas that are water, deserts and other zones where there is no people living.
- Make subdivisions of the squares in WorldArea to improve the precision of the cache in the future. The divisions can be done differently for different areas in the world. Some could require bigger divisions and some smaller.
- Solve the issue with polygon boundaries. The points belonging to the polygon boundaries are not included in the polygon area.
## Usage
Project can be built with:
```sh
make build
```
Then the containers started with:
```sh
make up
```
And then the tests can be run with:
```sh
make test
```
## References
- [GeoDjango] - GeoDjango tutorial from the official Django docs.
[GeoDjango]: <https://docs.djangoproject.com/en/4.0/ref/contrib/gis/>

@ -0,0 +1,29 @@
import json
from apps.providers.models import ServiceArea
from apps.utils import get_redis_client
from ..models import WorldArea
def populate_cache():
redis_instance = get_redis_client()
for service_area in ServiceArea.objects.all():
intersection_qs = WorldArea.objects.filter(square__intersects=service_area.polygon)
if not intersection_qs.exists():
continue
for area in intersection_qs:
area_code = area.code
value = redis_instance.get(area_code)
if not value:
value = list()
else:
value = json.loads(value)
value.append(service_area.id)
redis_instance.set(area.code, json.dumps(value))

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ParameterConfig(AppConfig):
default_auto_field = 'django.db.models.AutoField'
name = 'apps.parameters'

@ -0,0 +1,23 @@
# Generated by Django 3.2.13 on 2022-06-03 03:12
import django.contrib.gis.db.models.fields
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='WorldArea',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(max_length=10, unique=True)),
('square', django.contrib.gis.db.models.fields.PolygonField(srid=4326)),
],
),
]

@ -0,0 +1,34 @@
from django.contrib.gis.geos import Polygon
from django.db import migrations
def populate_world_areas(apps, schema_editor):
WorldArea = apps.get_model('parameters', 'WorldArea')
world_areas = list()
for latitude in range(-90, 90):
for longitude in range(-180, 180):
world_areas.append(
WorldArea(
square=Polygon([
[latitude, longitude],
[latitude, longitude + 1],
[latitude + 1, longitude + 1],
[latitude + 1, longitude],
[latitude, longitude]
]),
code=f'{latitude}_{longitude}')
)
_ = WorldArea.objects.bulk_create(world_areas)
class Migration(migrations.Migration):
dependencies = [
('parameters', '0001_initial'),
]
operations = [
migrations.RunPython(populate_world_areas)
]

@ -0,0 +1 @@
from .world_area import WorldArea

@ -0,0 +1,11 @@
from django.contrib.gis.db import models
class WorldArea(models.Model):
code = models.CharField(
max_length=10,
blank=False,
null=False,
unique=True
)
square = models.PolygonField()

@ -0,0 +1,17 @@
import json
import redis
from typing import Union, Set
from apps.utils import get_redis_client
def get_object_from_cache(cache_key: str, /, *, redis_instance: redis.StrictRedis = None) -> Union[Set[int], list]:
if not redis_instance:
redis_instance = get_redis_client()
value = redis_instance.get(cache_key)
if not value:
return []
return set(json.loads(value))

@ -0,0 +1,70 @@
import json
import redis
from django.contrib.gis.geos import Polygon
from django.db.models import QuerySet
from typing import Optional
from apps.parameters.models import WorldArea
from apps.utils import get_redis_client
from ..models import ServiceArea
def add_new_polygon_to_cache(service_area: ServiceArea, /, *, delete_polygon: Optional[Polygon] = None):
redis_instance = get_redis_client()
intersection_qs = WorldArea.objects.filter(square__intersects=service_area.polygon)
if intersection_qs.exists():
for area in intersection_qs:
area_code = area.code
value = redis_instance.get(area_code)
if not value:
value = list()
else:
value = json.loads(value)
value.append(service_area.id)
redis_instance.set(area.code, json.dumps(value))
if delete_polygon:
delete_polygon_from_cache(service_area, delete_polygon, redis_instance=redis_instance)
def delete_polygon_from_cache(
service_area: ServiceArea,
delete_polygon: Polygon,
/,
*,
redis_instance: redis.StrictRedis = None):
if not redis_instance:
redis_instance = get_redis_client()
intersection_qs = WorldArea.objects.filter(square__intersects=delete_polygon)
for area in intersection_qs:
area_code = area.code
value = redis_instance.get(area_code)
if not value:
continue
value = set(json.loads(value))
try:
value.remove(service_area.id)
except KeyError:
continue
value = list(value)
redis_instance.set(area.code, json.dumps(value))
def delete_polygon_batch_from_cache(service_areas: QuerySet):
redis_instance = get_redis_client()
for service_area in service_areas:
delete_polygon_from_cache(service_area, service_area.polygon, redis_instance=redis_instance)

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ProviderConfig(AppConfig):
default_auto_field = 'django.db.models.AutoField'
name = 'apps.providers'

@ -0,0 +1,39 @@
# Generated by Django 3.2.13 on 2022-06-02 22:05
import django.contrib.gis.db.models.fields
from django.db import migrations, models
import django.db.models.deletion
import phonenumber_field.modelfields
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Provider',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('currency', models.CharField(max_length=3)),
('email', models.EmailField(max_length=254)),
('language', models.CharField(max_length=2)),
('name', models.CharField(max_length=200)),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region=None)),
('timestamp', models.DateTimeField(auto_now_add=True, db_index=True)),
],
),
migrations.CreateModel(
name='ServiceArea',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('price', models.FloatField(default=0.0)),
('polygon', django.contrib.gis.db.models.fields.PolygonField(srid=4326)),
('provider', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='providers.provider')),
],
),
]

@ -0,0 +1,2 @@
from .area import ServiceArea
from .provider import Provider

@ -0,0 +1,23 @@
from django.contrib.gis.db import models
from .provider import Provider
class ServiceArea(models.Model):
name = models.CharField(
max_length=200,
blank=False,
null=False
)
price = models.FloatField(
default=0.0
)
polygon = models.PolygonField()
provider = models.ForeignKey(
Provider,
on_delete=models.DO_NOTHING,
blank=False,
null=False
)

@ -0,0 +1,36 @@
from django.db import models
from phonenumber_field.modelfields import PhoneNumberField
class Provider(models.Model):
# ISO 4217
currency = models.CharField(
max_length=3,
blank=False,
null=False
)
email = models.EmailField(
blank=False,
null=False
)
# ISO 639-3
language = models.CharField(
max_length=2,
blank=False,
null=False
)
name = models.CharField(
max_length=200,
blank=False,
null=False
)
phone_number = PhoneNumberField(
blank=False,
null=False
)
timestamp = models.DateTimeField(
auto_now_add=True,
db_index=True
)

@ -0,0 +1,2 @@
from .area import AreaValidationSerializer, ServiceAreaSerializer, AreaUpdateValidationSerializer
from .provider import ProviderSerializer

@ -0,0 +1,69 @@
from rest_framework import serializers
from django.contrib.gis.geos import Polygon
from django.contrib.gis.geos.error import GEOSException
from rest_framework.exceptions import ValidationError
from ..models import ServiceArea
class AreaValidationSerializer(serializers.ModelSerializer):
price = serializers.FloatField(required=True)
class Meta:
model = ServiceArea
fields = '__all__'
def validate_name(self, value):
if not value:
raise ValidationError('Field name is required.')
return value
def validate_provider(self, value):
try:
_ = int(value.id)
except ValueError:
raise ValidationError('Field provider has an incorrect type.')
return value.id
def validate_polygon(self, value):
try:
_ = Polygon(value)
except GEOSException:
raise ValidationError('Invalid polygon.')
return value
class AreaUpdateValidationSerializer(AreaValidationSerializer):
price = serializers.FloatField(required=True)
class Meta:
model = ServiceArea
fields = '__all__'
def validate_provider(self, value):
try:
_ = int(value.id)
except ValueError:
raise ValidationError('Field provider has an incorrect type.')
obj_provider_id = self.context.get('area_provider_id')
if value.id != obj_provider_id:
raise ValidationError('Provider does not match.')
return value.id
class ServiceAreaSerializer(serializers.ModelSerializer):
class Meta:
model = ServiceArea
fields = '__all__'
def to_representation(self, instance):
data = super(ServiceAreaSerializer, self).to_representation(instance)
data['polygon'] = instance.polygon.coords[0]
return data

@ -0,0 +1,31 @@
import pycountry
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from ..models import Provider
class ProviderSerializer(serializers.ModelSerializer):
class Meta:
model = Provider
fields = '__all__'
read_only_fields = ('id', 'timestamp')
def validate_language(self, value):
try:
_ = pycountry.languages.lookup(value)
except LookupError:
raise ValidationError(f'Invalid value for language: {value}.')
return value
def validate_currency(self, value):
try:
_ = pycountry.currencies.lookup(value)
except LookupError:
raise ValidationError(f'Invalid value for currency: {value}.')
return value

@ -0,0 +1,246 @@
from django.contrib.gis.geos import Polygon
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from unittest import mock
from ..models import Provider, ServiceArea
class FindServiceAreaCreateTests(APITestCase):
endpoint = reverse('find-service-area')
@classmethod
def setUpTestData(cls):
super(FindServiceAreaCreateTests, cls).setUpTestData()
cls.provider = Provider.objects.create(**{
'currency': 'USD',
'email': 'nicolas@email.com',
'language': 'EN',
'name': 'Test',
'phone_number': '+34685061901'
})
cls.another_provider = Provider.objects.create(**{
'currency': 'USD',
'email': 'another@email.com',
'language': 'SP',
'name': 'Test 2',
'phone_number': '+34685061456'
})
cls.last_provider = Provider.objects.create(**{
'currency': 'USD',
'email': 'last@email.com',
'language': 'FR',
'name': 'Test 3',
'phone_number': '+34685061776'
})
cls.service_area = ServiceArea.objects.create(
polygon=Polygon([[0.0, 0.0], [0.0, 50.0], [50.0, 50.0], [50.0, 0.0], [0.0, 0.0]]),
provider_id=cls.provider.id,
price=500.5,
name='Test area'
)
cls.another_service_area = ServiceArea.objects.create(
polygon=Polygon([[50.0, 50.0], [50.0, 100.0], [100.0, 100.0], [100.0, 50.0], [50.0, 50.0]]),
provider_id=cls.another_provider.id,
price=85.5,
name='Test area 2'
)
cls.last_service_area = ServiceArea.objects.create(
polygon=Polygon([[75.0, 50.0], [75.0, 75.0], [100.0, 75.0], [100.0, 50.0], [75.0, 50.0]]),
provider_id=cls.last_provider.id,
price=5.2,
name='Test area 2'
)
def setUp(self) -> None:
super(FindServiceAreaCreateTests, self).setUp()
self.get_object_from_cache_mock = mock.patch(
'apps.providers.views.find_service_area.get_object_from_cache'
).start()
self.get_object_from_cache_mock.return_value = True
def tearDown(self) -> None:
super(FindServiceAreaCreateTests, self).tearDown()
self.get_object_from_cache_mock.reset_mock()
mock.patch.stopall()
def test_find_service_area_case_one_result(self):
self.get_object_from_cache_mock.return_value = {self.service_area.id}
querystring = '?latitude=25.0&longitude=25.0'
response = self.client.get(
self.endpoint + querystring
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_data = response.json()
self.assertEqual(response_data['count'], 1)
self.assertEqual(response_data['results'][0]['id'], self.service_area.id)
self.get_object_from_cache_mock.assert_called_once()
def test_find_service_area_case_boundary_zero_results(self):
self.get_object_from_cache_mock.return_value = []
querystring = '?latitude=50.0&longitude=50.0'
response = self.client.get(
self.endpoint + querystring
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_data = response.json()
self.assertEqual(response_data['count'], 0)
self.get_object_from_cache_mock.assert_called_once()
def test_find_service_area_case_outside_zero_results(self):
self.get_object_from_cache_mock.return_value = []
querystring = '?latitude=-40.0&longitude=0.0'
response = self.client.get(
self.endpoint + querystring
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_data = response.json()
self.assertEqual(response_data['count'], 0)
self.get_object_from_cache_mock.assert_called_once()
def test_find_service_area_case_inside_two_results(self):
self.get_object_from_cache_mock.return_value = {self.another_service_area.id, self.last_service_area.id}
querystring = '?latitude=80.0&longitude=60.0'
response = self.client.get(
self.endpoint + querystring
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_data = response.json()
expected_ids = {self.another_service_area.id, self.last_service_area.id}
self.assertEqual(response_data['count'], len(expected_ids))
for result in response_data['results']:
self.assertIn(result['id'], expected_ids)
self.get_object_from_cache_mock.assert_called_once()
# failure tests
def test_find_service_area_failure_missing_latitude(self):
querystring = '?longitude=25.0'
response = self.client.get(
self.endpoint + querystring
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('latitude', response_data)
self.assertEqual(response_data['latitude'], 'Invalid value: None.')
self.get_object_from_cache_mock.assert_not_called()
def test_find_service_area_failure_missing_longitude(self):
querystring = '?latitude=25.0'
response = self.client.get(
self.endpoint + querystring
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('longitude', response_data)
self.assertEqual(response_data['longitude'], 'Invalid value: None.')
self.get_object_from_cache_mock.assert_not_called()
def test_find_service_area_failure_invalid_value_for_latitude(self):
invalid_value = 'aa'
querystring = f'?latitude={invalid_value}&longitude=25.0'
response = self.client.get(
self.endpoint + querystring
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('latitude', response_data)
self.assertEqual(response_data['latitude'], f'Invalid value: {invalid_value}.')
self.get_object_from_cache_mock.assert_not_called()
def test_find_service_area_failure_invalid_latitude_lower(self):
invalid_value = -94.1235
querystring = f'?latitude={invalid_value}&longitude=25.0'
response = self.client.get(
self.endpoint + querystring
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('latitude', response_data)
self.assertEqual(response_data['latitude'], f'Invalid value: {invalid_value}.')
self.get_object_from_cache_mock.assert_not_called()
def test_find_service_area_failure_invalid_latitude_greater(self):
invalid_value = 90.1235
querystring = f'?latitude={invalid_value}&longitude=25.0'
response = self.client.get(
self.endpoint + querystring
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('latitude', response_data)
self.assertEqual(response_data['latitude'], f'Invalid value: {invalid_value}.')
self.get_object_from_cache_mock.assert_not_called()
def test_find_service_area_failure_invalid_value_for_longitude(self):
invalid_value = 'aa'
querystring = f'?latitude=25.0&longitude={invalid_value}'
response = self.client.get(
self.endpoint + querystring
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('longitude', response_data)
self.assertEqual(response_data['longitude'], f'Invalid value: {invalid_value}.')
self.get_object_from_cache_mock.assert_not_called()
def test_find_service_area_failure_invalid_longitude_lower(self):
invalid_value = -200.0123
querystring = f'?latitude=25.0&longitude={invalid_value}'
response = self.client.get(
self.endpoint + querystring
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('longitude', response_data)
self.assertEqual(response_data['longitude'], f'Invalid value: {invalid_value}.')
self.get_object_from_cache_mock.assert_not_called()
def test_find_service_area_failure_invalid_longitude_greater(self):
invalid_value = 190.0123
querystring = f'?latitude=25.0&longitude={invalid_value}'
response = self.client.get(
self.endpoint + querystring
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('longitude', response_data)
self.assertEqual(response_data['longitude'], f'Invalid value: {invalid_value}.')
self.get_object_from_cache_mock.assert_not_called()

@ -0,0 +1,200 @@
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
class ProvidersCreateTests(APITestCase):
endpoint = reverse('provider-list')
def test_providers_create_success(self):
payload = {
'currency': 'USD',
'email': 'nicolas@email.com',
'language': 'EN',
'name': 'Test',
'phone_number': '+34685061901'
}
response = self.client.post(
self.endpoint,
payload,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response_data = response.json()
for field in payload.keys():
self.assertIn(field, response_data)
self.assertEqual(payload[field], response_data[field])
self.assertIn('id', response_data)
self.assertIn('timestamp', response_data)
# failure tests
def test_providers_create_failure_missing_field_currency(self):
payload = {
'email': 'nicolas@email.com',
'language': 'EN',
'name': 'Test',
'phone_number': '+34685061901'
}
response = self.client.post(
self.endpoint,
payload,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('currency', response_data)
self.assertEqual(response_data['currency'][0], 'This field is required.')
def test_providers_create_failure_missing_field_email(self):
payload = {
'currency': 'USD',
'language': 'EN',
'name': 'Test',
'phone_number': '+34685061901'
}
response = self.client.post(
self.endpoint,
payload,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('email', response_data)
self.assertEqual(response_data['email'][0], 'This field is required.')
def test_providers_create_failure_missing_field_language(self):
payload = {
'currency': 'USD',
'email': 'nicolas@email.com',
'name': 'Test',
'phone_number': '+34685061901'
}
response = self.client.post(
self.endpoint,
payload,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('language', response_data)
self.assertEqual(response_data['language'][0], 'This field is required.')
def test_providers_create_failure_missing_field_name(self):
payload = {
'currency': 'USD',
'email': 'nicolas@email.com',
'language': 'EN',
'phone_number': '+34685061901'
}
response = self.client.post(
self.endpoint,
payload,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('name', response_data)
self.assertEqual(response_data['name'][0], 'This field is required.')
def test_providers_create_failure_missing_field_phone_number(self):
payload = {
'currency': 'USD',
'email': 'nicolas@email.com',
'language': 'EN',
'name': 'Test'
}
response = self.client.post(
self.endpoint,
payload,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('phone_number', response_data)
self.assertEqual(response_data['phone_number'][0], 'This field is required.')
def test_providers_create_failure_invalid_currency(self):
payload = {
'currency': '55',
'email': 'nicolas@email.com',
'language': 'EN',
'name': 'Test',
'phone_number': '+34685061901'
}
response = self.client.post(
self.endpoint,
payload,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('currency', response_data)
self.assertEqual(response_data['currency'][0], f'Invalid value for currency: {payload["currency"]}.')
def test_providers_create_failure_invalid_email(self):
payload = {
'currency': 'USD',
'email': 'nicolasemail.com',
'language': 'EN',
'name': 'Test',
'phone_number': '+34685061901'
}
response = self.client.post(
self.endpoint,
payload,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('email', response_data)
self.assertEqual(response_data['email'][0], 'Enter a valid email address.')
def test_providers_create_failure_invalid_language(self):
payload = {
'currency': 'USD',
'email': 'nicolas@email.com',
'language': 'T',
'name': 'Test',
'phone_number': '+34685061901'
}
response = self.client.post(
self.endpoint,
payload,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('language', response_data)
self.assertEqual(response_data['language'][0], f'Invalid value for language: {payload["language"]}.')
def test_providers_create_failure_invalid_phone_number(self):
payload = {
'currency': 'USD',
'email': 'nicolas@email.com',
'language': 'EN',
'name': 'Test',
'phone_number': 'invalid_phone'
}
response = self.client.post(
self.endpoint,
payload,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('phone_number', response_data)
self.assertEqual(response_data['phone_number'][0], 'The phone number entered is not valid.')

@ -0,0 +1,70 @@
from django.contrib.gis.geos import Polygon
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from unittest import mock
from ..models import Provider, ServiceArea
class ProvidersDeleteTests(APITestCase):
endpoint_name = 'provider-detail'
@classmethod
def setUpTestData(cls):
super(ProvidersDeleteTests, cls).setUpTestData()
payload = {
'currency': 'USD',
'email': 'nicolas@email.com',
'language': 'EN',
'name': 'Test',
'phone_number': '+34685061901'
}
cls.provider = Provider.objects.create(**payload)
cls.service_area = ServiceArea.objects.create(
polygon=Polygon([[0.0, 0.0], [0.0, 50.0], [50.0, 50.0], [50.0, 0.0], [0.0, 0.0]]),
provider_id=cls.provider.id,
price=500.5,
name='Test area'
)
def setUp(self) -> None:
super(ProvidersDeleteTests, self).setUp()
self.delete_polygon_batch_from_cache_mock = mock.patch(
'apps.providers.views.provider.delete_polygon_batch_from_cache'
).start()
self.delete_polygon_batch_from_cache_mock.return_value = True
def tearDown(self) -> None:
super(ProvidersDeleteTests, self).tearDown()
self.delete_polygon_batch_from_cache_mock.reset_mock()
mock.patch.stopall()
def test_provider_delete_success(self):
endpoint = reverse(self.endpoint_name, kwargs={'pk': self.provider.id})
provider_id = self.provider.id
service_area_id = self.service_area.id
response = self.client.delete(
endpoint
)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertFalse(Provider.objects.filter(pk=provider_id).exists())
self.assertFalse(ServiceArea.objects.filter(pk=service_area_id).exists())
self.delete_polygon_batch_from_cache_mock.assert_called_once()
def test_provider_delete_failure_does_not_exist(self):
endpoint = reverse(self.endpoint_name, kwargs={'pk': self.provider.id + 100})
response = self.client.delete(
endpoint
)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.delete_polygon_batch_from_cache_mock.assert_not_called()

@ -0,0 +1,44 @@
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from ..models import Provider
class ProvidersUpdateTests(APITestCase):
endpoint = reverse('provider-list')
@classmethod
def setUpTestData(cls):
super(ProvidersUpdateTests, cls).setUpTestData()
cls.provider = Provider.objects.create(**{
'currency': 'USD',
'email': 'nicolas@email.com',
'language': 'EN',
'name': 'Test',
'phone_number': '+34685061901'
})
cls.provider_two = Provider.objects.create(**{
'currency': 'EUR',
'email': 'user@email.com',
'language': 'ES',
'name': 'Test 2',
'phone_number': '+34685065551'
})
def test_provider_list(self):
response = self.client.get(
self.endpoint
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_data = response.json()
expected_ids = {self.provider.id, self.provider_two.id}
self.assertEqual(response_data['count'], len(expected_ids))
results = response_data['results']
for provider in results:
self.assertIn(provider['id'], expected_ids)

@ -0,0 +1,41 @@
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from ..models import Provider
class ProvidersRetrieveTests(APITestCase):
endpoint_name = 'provider-detail'
@classmethod
def setUpTestData(cls):
super(ProvidersRetrieveTests, cls).setUpTestData()
payload = {
'currency': 'USD',
'email': 'nicolas@email.com',
'language': 'EN',
'name': 'Test',
'phone_number': '+34685061901'
}
cls.provider = Provider.objects.create(**payload)
def test_provider_retrieve_success(self):
endpoint = reverse(self.endpoint_name, kwargs={'pk': self.provider.id})
response = self.client.get(
endpoint
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_data = response.json()
self.assertEqual(response_data['id'], self.provider.id)
def test_provider_retrieve_failure_does_not_exist(self):
endpoint = reverse(self.endpoint_name, kwargs={'pk': self.provider.id + 100})
response = self.client.get(
endpoint
)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

@ -0,0 +1,52 @@
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from ..models import Provider
class ProvidersUpdateTests(APITestCase):
endpoint_name = 'provider-detail'
@classmethod
def setUpTestData(cls):
super(ProvidersUpdateTests, cls).setUpTestData()
payload = {
'currency': 'USD',
'email': 'nicolas@email.com',
'language': 'EN',
'name': 'Test',
'phone_number': '+34685061901'
}
cls.provider = Provider.objects.create(**payload)
def test_provider_update_success(self):
endpoint = reverse(self.endpoint_name, kwargs={'pk': self.provider.id})
payload = {
'currency': 'EUR',
'email': 'user@email.com',
'language': 'ES',
'name': 'Test 2',
'phone_number': '+34685061111'
}
response = self.client.put(
endpoint,
payload,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_data = response.json()
for field in payload.keys():
self.assertEqual(payload[field], response_data[field])
def test_provider_update_failure_does_not_exist(self):
endpoint = reverse(self.endpoint_name, kwargs={'pk': self.provider.id + 100})
response = self.client.put(
endpoint
)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

@ -0,0 +1,179 @@
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from unittest import mock
from ..models import Provider, ServiceArea
class ServiceAreaCreateTests(APITestCase):
endpoint = reverse('service-area-list')
@classmethod
def setUpTestData(cls):
super(ServiceAreaCreateTests, cls).setUpTestData()
cls.payload = {
'currency': 'USD',
'email': 'nicolas@email.com',
'language': 'EN',
'name': 'Test',
'phone_number': '+34685061901'
}
cls.provider = Provider.objects.create(**cls.payload)
def setUp(self) -> None:
super(ServiceAreaCreateTests, self).setUp()
self.add_new_polygon_to_cache_mock = mock.patch('apps.providers.views.area.add_new_polygon_to_cache').start()
self.add_new_polygon_to_cache_mock.return_value = True
def tearDown(self) -> None:
super(ServiceAreaCreateTests, self).tearDown()
self.add_new_polygon_to_cache_mock.reset_mock()
mock.patch.stopall()
def test_service_area_create_success(self):
payload = {
'name': 'Test area',
'price': 500.5,
'provider': self.provider.id,
'polygon': [[0.0, 0.0], [0.0, 50.0], [50.0, 50.0], [50.0, 0.0], [0.0, 0.0]]
}
response = self.client.post(
self.endpoint,
payload,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
service_area_qs = ServiceArea.objects.all()
self.assertEqual(service_area_qs.count(), 1)
service_area_obj = service_area_qs.first()
self.assertEqual(service_area_obj.provider_id, payload['provider'])
self.assertEqual(service_area_obj.name, payload['name'])
self.assertEqual(service_area_obj.price, payload['price'])
self.assertEqual(service_area_obj.polygon.num_points, len(payload['polygon']))
self.add_new_polygon_to_cache_mock.assert_called_once()
# failure tests
def test_service_area_create_failure_missing_required_field_name(self):
payload = {
'price': 500.5,
'provider': self.provider.id,
'polygon': [[0.0, 0.0], [0.0, 50.0], [50.0, 50.0], [50.0, 0.0], [0.0, 0.0]]
}
response = self.client.post(
self.endpoint,
payload,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('name', response_data)
self.assertEqual(response_data['name'][0], 'This field is required.')
self.add_new_polygon_to_cache_mock.assert_not_called()
def test_service_area_create_failure_missing_required_field_price(self):
payload = {
'name': 'Test area',
'provider': self.provider.id,
'polygon': [[0.0, 0.0], [0.0, 50.0], [50.0, 50.0], [50.0, 0.0], [0.0, 0.0]]
}
response = self.client.post(
self.endpoint,
payload,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('price', response_data)
self.assertEqual(response_data['price'][0], 'This field is required.')
self.add_new_polygon_to_cache_mock.assert_not_called()
def test_service_area_create_failure_missing_required_field_provider(self):
payload = {
'name': 'Test area',
'price': 500.5,
'polygon': [[0.0, 0.0], [0.0, 50.0], [50.0, 50.0], [50.0, 0.0], [0.0, 0.0]]
}
response = self.client.post(
self.endpoint,
payload,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('provider', response_data)
self.assertEqual(response_data['provider'][0], 'This field is required.')
self.add_new_polygon_to_cache_mock.assert_not_called()
def test_service_area_create_failure_missing_required_field_polygon(self):
payload = {
'name': 'Test area',
'price': 500.5,
'provider': self.provider.id
}
response = self.client.post(
self.endpoint,
payload,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('polygon', response_data)
self.assertEqual(response_data['polygon'][0], 'This field is required.')
self.add_new_polygon_to_cache_mock.assert_not_called()
def test_service_area_create_failure_invalid_provider_type(self):
payload = {
'name': 'Test area',
'price': 500.5,
'provider': 'provider',
'polygon': [[0.0, 0.0], [0.0, 50.0], [50.0, 50.0], [50.0, 0.0], [0.0, 0.0]]
}
response = self.client.post(
self.endpoint,
payload,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('provider', response_data)
self.assertEqual(response_data['provider'][0], 'Incorrect type. Expected pk value, received str.')
self.add_new_polygon_to_cache_mock.assert_not_called()
def test_service_area_create_failure_invalid_provider_id(self):
invalid_id = self.provider.id + 100
payload = {
'name': 'Test area',
'price': 500.5,
'provider': invalid_id,
'polygon': [[0.0, 0.0], [0.0, 50.0], [50.0, 50.0], [50.0, 0.0], [0.0, 0.0]]
}
response = self.client.post(
self.endpoint,
payload,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('provider', response_data)
self.assertEqual(response_data['provider'][0], f'Invalid pk "{invalid_id}" - object does not exist.')
self.add_new_polygon_to_cache_mock.assert_not_called()

@ -0,0 +1,67 @@
from django.contrib.gis.geos import Polygon
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from unittest import mock
from ..models import Provider, ServiceArea
class ServiceAreaDeleteTests(APITestCase):
endpoint_name = 'service-area-detail'
@classmethod
def setUpTestData(cls):
super(ServiceAreaDeleteTests, cls).setUpTestData()
cls.provider = Provider.objects.create(**{
'currency': 'USD',
'email': 'nicolas@email.com',
'language': 'EN',
'name': 'Test',
'phone_number': '+34685061901'
})
cls.service_area = ServiceArea.objects.create(
polygon=Polygon([[0.0, 0.0], [0.0, 50.0], [50.0, 50.0], [50.0, 0.0], [0.0, 0.0]]),
provider_id=cls.provider.id,
price=500.5,
name='Test area'
)
def setUp(self) -> None:
super(ServiceAreaDeleteTests, self).setUp()
self.delete_polygon_from_cache_from_cache_mock = mock.patch(
'apps.providers.views.area.delete_polygon_from_cache'
).start()
self.delete_polygon_from_cache_from_cache_mock.return_value = True
def tearDown(self) -> None:
super(ServiceAreaDeleteTests, self).tearDown()
self.delete_polygon_from_cache_from_cache_mock.reset_mock()
mock.patch.stopall()
def test_service_area_delete_success(self):
service_area_id = self.service_area.id
provider_id = self.provider.id
endpoint = reverse(self.endpoint_name, kwargs={'pk': service_area_id})
response = self.client.delete(
endpoint
)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertFalse(ServiceArea.objects.filter(pk=service_area_id).exists())
self.assertTrue(Provider.objects.filter(pk=provider_id).exists())
self.delete_polygon_from_cache_from_cache_mock.assert_called_once()
def test_service_area_delete_failure_invalid_id(self):
endpoint = reverse(self.endpoint_name, kwargs={'pk': self.service_area.id + 100})
response = self.client.delete(
endpoint
)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.delete_polygon_from_cache_from_cache_mock.assert_not_called()

@ -0,0 +1,58 @@
from django.contrib.gis.geos import Polygon
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from ..models import Provider, ServiceArea
class ServiceAreaCreateTests(APITestCase):
endpoint = reverse('service-area-list')
@classmethod
def setUpTestData(cls):
super(ServiceAreaCreateTests, cls).setUpTestData()
cls.provider = Provider.objects.create(**{
'currency': 'USD',
'email': 'nicolas@email.com',
'language': 'EN',
'name': 'Test',
'phone_number': '+34685061901'
})
cls.another_provider = Provider.objects.create(**{
'currency': 'USD',
'email': 'another@email.com',
'language': 'SP',
'name': 'Test 2',
'phone_number': '+34685061456'
})
cls.service_area = ServiceArea.objects.create(
polygon=Polygon([[0.0, 0.0], [0.0, 50.0], [50.0, 50.0], [50.0, 0.0], [0.0, 0.0]]),
provider_id=cls.provider.id,
price=500.5,
name='Test area'
)
cls.another_service_area = ServiceArea.objects.create(
polygon=Polygon([[50.0, 50.0], [50.0, 100.0], [100.0, 100.0], [100.0, 50.0], [50.0, 50.0]]),
provider_id=cls.another_provider.id,
price=85.5,
name='Test area 2'
)
def test_service_area_list(self):
response = self.client.get(
self.endpoint
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_data = response.json()
expected_ids = {self.service_area.id, self.another_service_area.id}
self.assertEqual(response_data['count'], len(expected_ids))
results = response_data['results']
for result in results:
self.assertIn(result['id'], expected_ids)

@ -0,0 +1,48 @@
from django.contrib.gis.geos import Polygon
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from ..models import Provider, ServiceArea
class ServiceAreaRetrieveTests(APITestCase):
endpoint_name = 'service-area-detail'
@classmethod
def setUpTestData(cls):
super(ServiceAreaRetrieveTests, cls).setUpTestData()
cls.provider = Provider.objects.create(**{
'currency': 'USD',
'email': 'nicolas@email.com',
'language': 'EN',
'name': 'Test',
'phone_number': '+34685061901'
})
cls.service_area = ServiceArea.objects.create(
polygon=Polygon([[0.0, 0.0], [0.0, 50.0], [50.0, 50.0], [50.0, 0.0], [0.0, 0.0]]),
provider_id=cls.provider.id,
price=500.5,
name='Test area'
)
def test_service_area_retrieve_success(self):
endpoint = reverse(self.endpoint_name, kwargs={'pk': self.service_area.id})
response = self.client.get(
endpoint
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_data = response.json()
self.assertEqual(response_data['id'], self.service_area.id)
def test_service_area_retrieve_failure_invalid_id(self):
endpoint = reverse(self.endpoint_name, kwargs={'pk': self.service_area.id + 100})
response = self.client.get(
endpoint
)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

@ -0,0 +1,116 @@
from django.contrib.gis.geos import Polygon
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from unittest import mock
from ..models import Provider, ServiceArea
class ServiceAreaUpdateTests(APITestCase):
endpoint_name = 'service-area-detail'
@classmethod
def setUpTestData(cls):
super(ServiceAreaUpdateTests, cls).setUpTestData()
cls.provider = Provider.objects.create(**{
'currency': 'USD',
'email': 'nicolas@email.com',
'language': 'EN',
'name': 'Test',
'phone_number': '+34685061901'
})
cls.another_provider = Provider.objects.create(**{
'currency': 'USD',
'email': 'another@email.com',
'language': 'SP',
'name': 'Test 2',
'phone_number': '+34685061456'
})
cls.service_area = ServiceArea.objects.create(
polygon=Polygon([[0.0, 0.0], [0.0, 50.0], [50.0, 50.0], [50.0, 0.0], [0.0, 0.0]]),
provider_id=cls.provider.id,
price=500.5,
name='Test area'
)
def setUp(self) -> None:
super(ServiceAreaUpdateTests, self).setUp()
self.add_new_polygon_to_cache_mock = mock.patch('apps.providers.views.area.add_new_polygon_to_cache').start()
self.add_new_polygon_to_cache_mock.return_value = True
def tearDown(self) -> None:
super(ServiceAreaUpdateTests, self).tearDown()
self.add_new_polygon_to_cache_mock.reset_mock()
mock.patch.stopall()
# NOTE ---- Failure tests related to serializer validation are done in the create test class and not repeated here
def test_service_area_update_success(self):
endpoint = reverse(self.endpoint_name, kwargs={'pk': self.service_area.id})
payload = {
'name': 'Test area 2',
'price': 12.5,
'provider': self.provider.id,
'polygon': [[50.0, 50.0], [50.0, 100.0], [100.0, 100.0], [100.0, 50.0], [50.0, 50.0]]
}
response = self.client.put(
endpoint,
payload,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.service_area.refresh_from_db()
self.assertEqual(self.service_area.name, payload['name'])
self.assertEqual(self.service_area.price, payload['price'])
self.assertEqual(self.service_area.provider_id, self.provider.id)
self.add_new_polygon_to_cache_mock.assert_called_once()
def test_service_area_update_failure_provider_does_not_match(self):
endpoint = reverse(self.endpoint_name, kwargs={'pk': self.service_area.id})
payload = {
'name': 'Test area 2',
'price': 12.5,
'provider': self.another_provider.id,
'polygon': [[50.0, 50.0], [50.0, 100.0], [100.0, 100.0], [100.0, 50.0], [50.0, 50.0]]
}
response = self.client.put(
endpoint,
payload,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_data = response.json()
self.assertIn('provider', response_data)
self.assertEqual(response_data['provider'][0], 'Provider does not match.')
self.add_new_polygon_to_cache_mock.assert_not_called()
def test_service_area_update_failure_not_found(self):
endpoint = reverse(self.endpoint_name, kwargs={'pk': self.service_area.id + 100})
payload = {
'name': 'Test area 2',
'price': 12.5,
'provider': self.another_provider.id,
'polygon': [[50.0, 50.0], [50.0, 100.0], [100.0, 100.0], [100.0, 50.0], [50.0, 50.0]]
}
response = self.client.put(
endpoint,
payload,
format='json'
)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.add_new_polygon_to_cache_mock.assert_not_called()

@ -0,0 +1,38 @@
from django.urls import path
from . import views
urlpatterns = [
path(
'',
views.ProvidersViewSet.as_view({'post': 'create', 'get': 'list'}),
name='provider-list'
),
path(
'<int:pk>/',
views.ProvidersViewSet.as_view({'put': 'update', 'get': 'retrieve', 'delete': 'destroy'}),
name='provider-detail'
),
path(
'service-area/',
views.ServiceAreaViewSet.as_view({'post': 'create_service_area', 'get': 'list'}),
name='service-area-list'
),
path(
'service-area/<int:pk>/',
views.ServiceAreaViewSet.as_view({'put': 'update_service_area', 'get': 'retrieve', 'delete': 'destroy'}),
name='service-area-detail'
),
path(
'service-area/point/',
views.FindServiceAreaView.as_view({'get': 'find_service_areas'}),
name='find-service-area'
),
]

@ -0,0 +1,3 @@
from .area import ServiceAreaViewSet
from .find_service_area import FindServiceAreaView
from .provider import ProvidersViewSet

@ -0,0 +1,62 @@
from rest_framework import status
from django.contrib.gis.geos import Polygon
from rest_framework.mixins import RetrieveModelMixin, ListModelMixin, DestroyModelMixin
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from ..actions.update_cache import add_new_polygon_to_cache, delete_polygon_from_cache
from ..models import ServiceArea
from ..serializers import AreaValidationSerializer, ServiceAreaSerializer, AreaUpdateValidationSerializer
class ServiceAreaViewSet(RetrieveModelMixin, ListModelMixin, DestroyModelMixin, GenericViewSet):
queryset = ServiceArea.objects.all()
serializer_class = ServiceAreaSerializer
def create_service_area(self, request, *args, **kwargs):
payload = request.data
serializer = AreaValidationSerializer(data=payload)
serializer.is_valid(raise_exception=True)
service_area_obj = ServiceArea.objects.create(
name=payload.get('name'),
price=payload.get('price'),
polygon=Polygon(payload.get('polygon')),
provider_id=payload.get('provider')
)
add_new_polygon_to_cache(service_area_obj)
return Response(status=status.HTTP_201_CREATED)
def update_service_area(self, request, *args, **kwargs):
service_area_obj = self.get_object()
payload = request.data
serializer = AreaUpdateValidationSerializer(
data=payload,
context={'area_provider_id': service_area_obj.provider_id}
)
serializer.is_valid(raise_exception=True)
old_polygon = service_area_obj.polygon
_ = ServiceArea.objects.filter(id=service_area_obj.id).update(
name=payload.get('name'),
price=payload.get('price'),
polygon=Polygon(payload.get('polygon'))
)
service_area_obj.refresh_from_db()
add_new_polygon_to_cache(service_area_obj, delete_polygon=old_polygon)
return Response(status=status.HTTP_200_OK)
def destroy(self, request, *args, **kwargs):
service_area_obj = self.get_object()
delete_polygon_from_cache(service_area_obj, service_area_obj.polygon)
return super(ServiceAreaViewSet, self).destroy(request)

@ -0,0 +1,74 @@
from rest_framework import status
from django.contrib.gis.geos import Point
from rest_framework.exceptions import ValidationError
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from typing import Optional, Tuple
from ..actions.get_object import get_object_from_cache
from ..models import ServiceArea
from ..serializers import ServiceAreaSerializer
class FindServiceAreaView(GenericViewSet):
serializer_class = ServiceAreaSerializer
def validate_querystring(self, latitude: Optional[str], longitude: Optional[str]) -> Optional[Tuple[float, float]]:
try:
lat = float(latitude)
except (TypeError, ValueError):
raise ValidationError({'latitude': f'Invalid value: {latitude}.'})
if lat < -90 or lat > 90:
raise ValidationError({'latitude': f'Invalid value: {lat}.'})
try:
lng = float(longitude)
except (TypeError, ValueError):
raise ValidationError({'longitude': f'Invalid value: {longitude}.'})
if lng < -180 or lng > 180:
raise ValidationError({'longitude': f'Invalid value: {lng}.'})
return lat, lng
def generate_cache_key(self, latitude, longitude):
latitude = int(latitude)
if latitude < 0:
latitude -= latitude
longitude = int(longitude)
if longitude < 0:
longitude -= 1
return f'{latitude}_{longitude}'
def find_service_areas(self, request, *args, **kwargs):
query_parameters = self.request.query_params
latitude = query_parameters.get('latitude')
longitude = query_parameters.get('longitude')
validated_coordinates = self.validate_querystring(latitude, longitude)
cache_key = self.generate_cache_key(*validated_coordinates)
service_area_ids = get_object_from_cache(cache_key)
point = Point(validated_coordinates)
cache_failure = False # TODO: Implement a check to activate fallback system
if cache_failure:
service_area_qs = ServiceArea.objects.filter(polygon__contains=point)
else:
service_area_qs = ServiceArea.objects.filter(id__in=service_area_ids).filter(polygon__contains=point)
page = self.paginate_queryset(service_area_qs)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(service_area_qs, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)

@ -0,0 +1,21 @@
from rest_framework.viewsets import ModelViewSet
from ..actions.update_cache import delete_polygon_batch_from_cache
from ..models import Provider, ServiceArea
from ..serializers import ProviderSerializer
class ProvidersViewSet(ModelViewSet):
serializer_class = ProviderSerializer
queryset = Provider.objects.all()
def destroy(self, request, *args, **kwargs):
provider_id = kwargs['pk']
service_area_qs = ServiceArea.objects.filter(provider_id=provider_id)
if service_area_qs.exists():
delete_polygon_batch_from_cache(service_area_qs)
_ = service_area_qs.delete()
return super(ProvidersViewSet, self).destroy(request)

@ -0,0 +1,12 @@
import redis
from django.conf import settings
def get_redis_client() -> redis.StrictRedis:
redis_client = redis.StrictRedis(
host=settings.REDIS_HOSTNAME,
port=settings.REDIS_PORT,
db=settings.REDIS_DB_NUMBER
)
return redis_client

@ -0,0 +1,43 @@
version: "2"
services:
db:
image: kartoza/postgis:14-3.2
volumes:
- db_data:/var/lib/postgres
environment:
POSTGRES_DB: shuttleapp
POSTGRES_USER: backoffice_user
POSTGRES_PASSWORD: qwerty
redis:
image: redis:3.0
volumes:
- redis:/data
ports:
- 6379:6379
command: redis-server --appendonly yes
shuttleapp:
build: .
restart: unless-stopped
depends_on:
- db
- redis
command: /env/bin/python /app/manage.py runserver 0.0.0.0:8091
env_file:
- variables.dev
environment:
- DJANGO_SETTINGS_MODULE=shuttleapp.settings.development
- RDS_HOSTNAME=db
- REDIS_HOSTNAME=redis
ports:
- 8091:8091
volumes:
- .:/app
volumes:
db_data:
driver: local
redis:
driver: local

@ -0,0 +1,2 @@
#!/bin/sh
/env/bin/python /app/manage.py migrate --noinput

@ -0,0 +1,5 @@
CREATE SCHEMA `shuttleapp`;
CREATE SCHEMA `test_shuttleapp`;
CREATE USER 'backoffice_user'@'%' WITH PASSWORD 'qwerty';
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA shuttleapp.* TO 'backoffice_user'@'%';
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA test_shuttleapp.* TO 'backoffice_user'@'%';

@ -6,7 +6,7 @@ import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shuttlebackoffice.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shuttleapp.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:

@ -0,0 +1,504 @@
{
"info": {
"_postman_id": "b0d4398c-4b29-4d1b-b2ed-66a87669d0cc",
"name": "z_shuttle",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "4159200"
},
"item": [
{
"name": "Provider",
"item": [
{
"name": "CREATE Provider",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code 201\", function () {",
" pm.response.to.have.status(201);",
"});",
"",
"pm.test(\"Save user ID\", function () {",
" let userId = pm.response.json().id;",
" pm.environment.set(\"USER_ID\", userId);",
"});",
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"currency\": \"USD\",\n \"email\": \"nicolas@email.com\",\n \"language\": \"EN\",\n \"name\": \"Provider #1\",\n \"phone_number\": \"+34685061901\"\n}"
},
"url": {
"raw": "http://127.0.0.1:8091/provider/",
"protocol": "http",
"host": [
"127",
"0",
"0",
"1"
],
"port": "8091",
"path": [
"provider",
""
]
}
},
"response": []
},
{
"name": "LIST Providers",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code 200\", function () {",
" pm.response.to.have.status(200);",
"});",
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://127.0.0.1:8091/provider/",
"protocol": "http",
"host": [
"127",
"0",
"0",
"1"
],
"port": "8091",
"path": [
"provider",
""
]
}
},
"response": []
},
{
"name": "RETRIEVE Provider",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code 200\", function () {",
" pm.response.to.have.status(200);",
"});",
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://127.0.0.1:8091/provider/{{USER_ID}}/",
"protocol": "http",
"host": [
"127",
"0",
"0",
"1"
],
"port": "8091",
"path": [
"provider",
"{{USER_ID}}",
""
]
}
},
"response": []
},
{
"name": "UPDATE Provider",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code 200\", function () {",
" pm.response.to.have.status(200);",
"});",
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "PUT",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"currency\": \"EUR\",\n \"email\": \"nico@email.com\",\n \"language\": \"ES\",\n \"name\": \"Provider #1.1\",\n \"phone_number\": \"+34685061900\"\n}"
},
"url": {
"raw": "http://127.0.0.1:8091/provider/{{USER_ID}}/",
"protocol": "http",
"host": [
"127",
"0",
"0",
"1"
],
"port": "8091",
"path": [
"provider",
"{{USER_ID}}",
""
]
}
},
"response": []
},
{
"name": "DELETE Provider",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code 204\", function () {",
" pm.response.to.have.status(204);",
"});",
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "http://127.0.0.1:8091/provider/{{USER_ID}}/",
"protocol": "http",
"host": [
"127",
"0",
"0",
"1"
],
"port": "8091",
"path": [
"provider",
"{{USER_ID}}",
""
]
}
},
"response": []
}
]
},
{
"name": "Service Area",
"item": [
{
"name": "CREATE Service Area",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code 201\", function () {",
" pm.response.to.have.status(201);",
"});",
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"name\": \"Test area\",\n \"price\": 500.5,\n \"provider\": {{USER_ID}},\n \"polygon\": [[0.0, 0.0], [0.0, 50.0], [50.0, 50.0], [50.0, 0.0], [0.0, 0.0]]\n}"
},
"url": {
"raw": "http://127.0.0.1:8091/provider/service-area/",
"protocol": "http",
"host": [
"127",
"0",
"0",
"1"
],
"port": "8091",
"path": [
"provider",
"service-area",
""
]
}
},
"response": []
},
{
"name": "LIST Service Areas",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code 200\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Save service area ID\", function () {",
" let serviceAreaId = pm.response.json()[\"results\"][0].id;",
" pm.environment.set(\"SERVICE_AREA_ID\", serviceAreaId);",
"});",
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://127.0.0.1:8091/provider/service-area/",
"protocol": "http",
"host": [
"127",
"0",
"0",
"1"
],
"port": "8091",
"path": [
"provider",
"service-area",
""
]
}
},
"response": []
},
{
"name": "RETRIEVE Service Area",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code 200\", function () {",
" pm.response.to.have.status(200);",
"});",
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://127.0.0.1:8091/provider/service-area/{{SERVICE_AREA_ID}}/",
"protocol": "http",
"host": [
"127",
"0",
"0",
"1"
],
"port": "8091",
"path": [
"provider",
"service-area",
"{{SERVICE_AREA_ID}}",
""
]
}
},
"response": []
},
{
"name": "UPDATE Service Area",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code 200\", function () {",
" pm.response.to.have.status(200);",
"});",
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "PUT",
"header": [
{
"key": "Content-Type",
"value": "application/json",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"name\": \"Test area 2\",\n \"price\": 12.5,\n \"provider\": {{USER_ID}},\n \"polygon\": [[50.0, 50.0], [50.0, 100.0], [100.0, 100.0], [100.0, 50.0], [50.0, 50.0]]\n}"
},
"url": {
"raw": "http://127.0.0.1:8091/provider/service-area/{{SERVICE_AREA_ID}}/",
"protocol": "http",
"host": [
"127",
"0",
"0",
"1"
],
"port": "8091",
"path": [
"provider",
"service-area",
"{{SERVICE_AREA_ID}}",
""
]
}
},
"response": []
},
{
"name": "DELETE Service Area",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code 204\", function () {",
" pm.response.to.have.status(204);",
"});",
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "http://127.0.0.1:8091/provider/service-area/{{SERVICE_AREA_ID}}/",
"protocol": "http",
"host": [
"127",
"0",
"0",
"1"
],
"port": "8091",
"path": [
"provider",
"service-area",
"{{SERVICE_AREA_ID}}",
""
]
}
},
"response": []
}
]
},
{
"name": "FIND Service Areas",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code 200\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Save service area ID\", function () {",
" let serviceAreaId = pm.response.json()[\"results\"][0].id;",
" pm.environment.set(\"SERVICE_AREA_ID\", serviceAreaId);",
"});",
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://127.0.0.1:8091/provider/service-area/point?latitude=20.000&longitude=25.1234",
"protocol": "http",
"host": [
"127",
"0",
"0",
"1"
],
"port": "8091",
"path": [
"provider",
"service-area",
"point"
],
"query": [
{
"key": "latitude",
"value": "20.000"
},
{
"key": "longitude",
"value": "25.1234"
}
]
}
},
"response": []
}
]
}

@ -0,0 +1,2 @@
-r requirements.txt
ipdb ~= 0.13.9

@ -0,0 +1,11 @@
Django==3.2.13
django-phonenumber-field==6.1.0
djangorestframework==3.13.1
gunicorn==20.1.0
phonenumbers==8.12.49
psycopg2-binary==2.9.1
pycountry==22.3.5
redis==3.0.0

@ -1,5 +1,5 @@
"""
ASGI config for shuttlebackoffice project.
ASGI config for shuttleapp project.
It exposes the ASGI callable as a module-level variable named ``application``.
@ -11,6 +11,6 @@ import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shuttlebackoffice.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shuttleapp.settings')
application = get_asgi_application()

@ -0,0 +1,8 @@
from django.http import HttpResponse
from django.utils.deprecation import MiddlewareMixin
class HealthCheckMiddleware(MiddlewareMixin):
def process_request(self, request):
if request.META['PATH_INFO'] == '/ping/':
return HttpResponse('OK')

@ -1,5 +1,5 @@
"""
Django settings for shuttlebackoffice project.
Django settings for shuttleapp project.
Generated by 'django-admin startproject' using Django 3.2.13.
@ -9,9 +9,20 @@ https://docs.djangoproject.com/en/3.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""
import os
import sys
from pathlib import Path
def getenv_or_fail(env_name, default_value=None):
value = os.getenv(env_name, default_value)
if value:
return value
print("ERROR: ENV variable named `{}` is not defined.".format(env_name))
sys.exit(1)
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
@ -20,7 +31,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-@(&r&=9w7ggdz34ioet7dnpq26!e64)f-%a@vj_e_)*spbw+06'
SECRET_KEY = getenv_or_fail('DJANGO_SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
@ -37,9 +48,14 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.gis',
'apps.providers.app.ProviderConfig',
'apps.parameters.app.ParameterConfig',
]
MIDDLEWARE = [
'shuttleapp.middleware.HealthCheckMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
@ -49,7 +65,7 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'shuttlebackoffice.urls'
ROOT_URLCONF = 'shuttleapp.urls'
TEMPLATES = [
{
@ -67,7 +83,7 @@ TEMPLATES = [
},
]
WSGI_APPLICATION = 'shuttlebackoffice.wsgi.application'
WSGI_APPLICATION = 'shuttleapp.wsgi.application'
# Database
@ -75,12 +91,15 @@ WSGI_APPLICATION = 'shuttlebackoffice.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': getenv_or_fail('RDS_DB_NAME'),
'USER': getenv_or_fail('RDS_USERNAME'),
'PASSWORD': getenv_or_fail('RDS_PASSWORD'),
'HOST': getenv_or_fail('RDS_HOSTNAME'),
'PORT': getenv_or_fail('RDS_PORT')
}
}
# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
@ -123,3 +142,16 @@ STATIC_URL = '/static/'
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
#
# GDAL_LIBRARY_PATH = '/usr/lib/lib/libgdal.so'
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20
}
# REDIS
REDIS_HOSTNAME = getenv_or_fail('REDIS_HOSTNAME')
REDIS_PORT = getenv_or_fail('REDIS_PORT', '6379')
REDIS_DB_NUMBER = os.environ.get('REDIS_DB_NUMBER', '')
REDIS_URL = "redis://{}:{}/{}".format(REDIS_HOSTNAME, REDIS_PORT, REDIS_DB_NUMBER)

@ -0,0 +1,8 @@
from shuttleapp.settings.default import *
ALLOWED_HOSTS += [
'*',
]
DEBUG = True
TEMPLATE_DEBUG = True

@ -1,4 +1,4 @@
"""shuttlebackoffice URL Configuration
"""shuttleapp URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.2/topics/http/urls/
@ -14,8 +14,10 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('provider/', include('apps.providers.urls')),
]

@ -1,5 +1,5 @@
"""
WSGI config for shuttlebackoffice project.
WSGI config for shuttleapp project.
It exposes the WSGI callable as a module-level variable named ``application``.
@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shuttlebackoffice.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shuttleapp.settings')
application = get_wsgi_application()

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

@ -0,0 +1,12 @@
DJANGO_SECRET_KEY='}x;#tt`a?:W;:3cqGYVC3(xB_t!tc2@P>;!_{<i)+`Dgv}P'
DJANGO_SETTINGS_MODULE=shuttleapp.settings.development
RDS_HOSTNAME=192.168.16.3
RDS_PORT=5432
RDS_DB_NAME=shuttleapp
RDS_USERNAME=backoffice_user
RDS_PASSWORD=qwerty
REDIS_HOSTNAME=192.168.16.2
REDIS_PORT=6379
REDIS_DB_NUMBER=0
Loading…
Cancel
Save