Skip to content

Commit

Permalink
final functionallity description tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dmitry-viskov committed Nov 21, 2015
1 parent 3116ad1 commit ab9d3d1
Show file tree
Hide file tree
Showing 14 changed files with 497 additions and 22 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 59,8 @@ target/
# idea
.idea/

#virtualenv
# virtualenv
venv

# settings.py
taxi_online_example/settings.py
89 changes: 87 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,2 1,87 @@
taxi-online-example
================================
Requirements
============

* Python 2.6 or 2.7
* virtualenv pip
* PostgreSQL server

Installation
============

At first you should install PostgreSQL (probably it will work on MySQL but I'm not sure).

The recommended way of deploying the current project is to use virtualenv:

.. code-block:: bash
mkdir venv && echo "Virtualenv directory" > venv/README
virtualenv --no-site-packages --prompt="(taxi_online_example)" venv
source venv/bin/activate
pip install -r requirements.txt
After you have created virtualenv directory you should create *settings.py* file to implement your own enviroment settings (at least you should change settings for connection to PostgreSQL):

.. code-block:: bash
cd taxi_online_example
cp settings.example.py settings.py
vim settings.py
Also you need to create empty database and user for the project:

.. code-block:: sql
CREATE DATABASE test_taxi_online_example;
CREATE USER taxi WITH password 'taxi';
ALTER USER taxi CREATEDB;
GRANT ALL privileges ON DATABASE test_taxi_online_example TO taxi;
After DB preparation will be finished you need to create empty DB structure:

.. code-block:: bash
source venv/bin/activate
python manage.py syncdb
How to run default Django web server
====================================

.. code-block:: bash
source venv/bin/activate
python manage.py runserver 0.0.0.0:8000
How to run unit tests
=====================

.. code-block:: bash
source venv/bin/activate
python manage.py test --keepdb -v 2
REST API
====================================

.. code-block:: bash
# to get information about the location of the taxi with ID 123
curl "http://0.0.0.0:8000/taxi/123/location/" -v
# to create or update the location of taxi with ID 123
curl --data "lat=56.312719&lon=43.845431" "http://0.0.0.0:8000/taxi/123/location/" -v
# to remove the location of taxi with ID 123 (the driver goes to sleep)
curl -X DELETE "http://0.0.0.0:8000/taxi/123/location/" -v
# to get information about the order for the passenger with ID 456
curl "http://0.0.0.0:8000/passenger/456/order/" -v
# to create or update new order that the passenger with ID 456 will be picked up just now
curl --data "lat=56.312719&lon=43.845431" "http://0.0.0.0:8000/passenger/456/order/" -v
# to create or update new order that the passenger with ID 456 will be picked up sometimes in future (unixtimestamp as a param)
curl --data "lat=56.312719&lon=43.845431&time_to_pick_up=1448577995" "http://0.0.0.0:8000/passenger/456/order/" -v
# to remove the order for the passenger with ID 456
curl -X DELETE "http://0.0.0.0:8000/passenger/456/order/" -v
Empty file added logs/.path_to_logs
Empty file.
1 change: 1 addition & 0 deletions taxi_online_example/management/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 1 @@
# -*- coding: utf-8 -*-
1 change: 1 addition & 0 deletions taxi_online_example/management/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 1 @@
# -*- coding: utf-8 -*-
46 changes: 46 additions & 0 deletions taxi_online_example/management/commands/process_orders.py
Original file line number Diff line number Diff line change
@@ -0,0 1,46 @@
# -*- coding: utf-8 -*-

import time
import logging
from django.core.management.base import BaseCommand
from taxi_online_example.service import process_passengers


logger = logging.getLogger('processing')


class Command(BaseCommand):

help = u'Process current orders (for usage as a CRON task)'
default_times_to_repeat = 1
default_timeout = 5

def add_arguments(self, parser):
parser.add_argument('--times_to_repeat',
action='store',
dest='times_to_repeat',
default=self.default_times_to_repeat,
help='how many times to repeat the process of getting orders for database')
parser.add_argument('--timeout_before_repeat',
action='store',
dest='timeout_before_repeat',
default=self.default_timeout,
help='timeout in seconds before repeat the search for the waiting passengers')

def handle(self, *args, **options):
times_to_repeat = self.default_times_to_repeat
if options['times_to_repeat']:
times_to_repeat = int(options['times_to_repeat'])

timeout_before_repeat = self.default_timeout
if options['timeout_before_repeat']:
timeout_before_repeat = int(options['timeout_before_repeat'])

for i in xrange(times_to_repeat):
print 'Attempt num ', str(i 1), ':'
print ''
process_passengers()
print ''

if (i 1) != times_to_repeat:
time.sleep(timeout_before_repeat)
40 changes: 40 additions & 0 deletions taxi_online_example/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 1,40 @@
# -*- coding: utf-8 -*-

import socket
import time
import logging
from taxi_online_example.settings import TESTING


logger = logging.getLogger('django.api.request')


class RequestLogMiddleware(object):

def process_request(self, request):
request.start_time = time.time()

def process_response(self, request, response):

if 'content-type' in response and response['content-type'] == 'application/json':
if getattr(response, 'streaming', False):
response_body = '<<<Streaming>>>'
else:
response_body = response.content
else:
response_body = '<<<Not JSON>>>'

if not TESTING:
log_data = {
'remote_address': request.META['REMOTE_ADDR'],
'server_hostname': socket.gethostname(),
'request_method': request.method,
'request_path': request.get_full_path(),
'request_body': request.body,
'response_status': response.status_code,
'response_body': response_body,
'run_time': time.time() - request.start_time,
}
logger.info(log_data)

return response
26 changes: 20 additions & 6 deletions taxi_online_example/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 3,8 @@
import datetime
from django.db import models
from django.core.validators import MaxValueValidator, MinValueValidator
from taxi_online_example.utils import date_now_or_future_validator
from taxi_online_example.utils import date_now_or_future_validator, UTC
from django.forms.models import model_to_dict


class TaxiLocation(models.Model):
Expand All @@ -20,6 21,9 @@ def change_activity(self, is_busy):
self.is_busy = is_busy
self.save()

def description(self):
return '<TaxiLocation %s>' % _get_model_object_description(self)


class PassengerOrder(models.Model):
passenger_id = models.CharField(max_length=200, unique=True, db_index=True)
Expand All @@ -40,12 44,15 @@ def remove_taxi(self):
self.taxi_id = None
self.save()

def get_all_passengers_for_pick_up(self):
pass
@classmethod
def get_all_passengers_for_pick_up(cls):
return cls.objects.filter(time_to_pick_up__lte=datetime.datetime.now(tz=UTC()),
taxi_id__isnull=True).order_by('time_to_pick_up')

def get_nearest_free_taxi(self, radius=10):
# http://www.plumislandmedia.net/mysql/haversine-mysql-nearest-loc/
sql = """SELECT tl.taxi_id,
sql = """SELECT tl.id,
tl.taxi_id,
p.distance_unit
* DEGREES(ACOS(COS(RADIANS(p.latpoint))
* COS(RADIANS(tl.lat))
Expand All @@ -67,12 74,19 @@ def get_nearest_free_taxi(self, radius=10):
BETWEEN p.longpoint - (p.radius / (p.distance_unit * COS(RADIANS(p.latpoint))))
AND p.longpoint (p.radius / (p.distance_unit * COS(RADIANS(p.latpoint))))
ORDER BY distance_in_km
LIMIT 10""" % {'taxi_location_table_name': TaxiLocation._meta.db_table,
LIMIT 1""" % {'taxi_location_table_name': TaxiLocation._meta.db_table,
'latpoint': self.lat, 'longpoint': self.lon, 'radius': radius}

return TaxiLocation.objects.raw(sql)
for p in TaxiLocation.objects.raw(sql):
return p
return False

def description(self):
return '<PassengerOrder %s>' % _get_model_object_description(self)


def _get_model_object_description(obj):
return ' '.join([('%s=%s' % (k, str(v))) for k, v in model_to_dict(obj).iteritems()])



39 changes: 39 additions & 0 deletions taxi_online_example/service.py
Original file line number Diff line number Diff line change
@@ -0,0 1,39 @@
# -*- coding: utf-8 -*-

import logging
import uuid
from django.db import IntegrityError, transaction
from taxi_online_example.models import PassengerOrder

logger = logging.getLogger('processing')


def process_passengers():
orders = PassengerOrder.get_all_passengers_for_pick_up()

for order in orders:
identifier = str(uuid.uuid4())

_log(identifier, 'Try to process order', order.description())

taxi = order.get_nearest_free_taxi()
if taxi:
_log(identifier, 'The nearest taxi is: ', taxi.description())
try:
with transaction.atomic():
order.taxi_id = taxi.taxi_id
order.save()
taxi.is_busy = True
taxi.save()
_log(identifier, 'The taxi was successfully assigned!')
except IntegrityError:
# in case of race condition this taxi became busy
# and database will rise an unique exception on field PassengerOrder.taxi_id
# don't do anything - this passenger will be processed on the next iteration
_log(identifier, 'Something wrong! The taxi wasn\'t assigned!')
else:
_log(identifier, 'Can\'t find the taxi')


def _log(identifier, *msgs):
logger.info(' '.join(msgs), extra={'identifier': identifier})
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 14,13 @@

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
import sys
import logging.config

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

TESTING = sys.argv[1:2] == ['test']

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
Expand Down Expand Up @@ -98,6 102,60 @@
]
}

LOGGING = {
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'verbose': {
'format': '[%(levelname)s][%(asctime)s][%(module)s][%(message)s]'
},
'simple': {
'format': '[%(levelname)s][%(identifier)s][%(message)s]'
},
},
'handlers': {
'console': {
'formatter': 'simple',
'level': 'DEBUG',
'class': 'logging.StreamHandler'
},
'django_requests': {
'level': 'DEBUG',
'class': 'logging.FileHandler',
'filename': '/'.join([BASE_DIR , 'logs', 'django.requests.log'])
},
'django_api_requests': {
'formatter': 'verbose',
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': '/'.join([BASE_DIR , 'logs', 'django.api.requests.log'])
},
'processing': {
'formatter': 'simple',
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': '/'.join([BASE_DIR , 'logs', 'processing.log'])
}
},
'loggers': {
'django.request': {
'handlers': ['django_requests'],
'level': 'DEBUG',
'propagate': True,
},
'django.api.request': {
'handlers': ['django_api_requests'],
'level': 'INFO',
'propagate': True,
},
'processing': {
'handlers': ['processing', 'console'],
'level': 'INFO',
'propagate': True,
}
},
}
logging.config.dictConfig(LOGGING)

# Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/
Expand Down
Loading

0 comments on commit ab9d3d1

Please sign in to comment.