Initial checkin
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
**pycache**
|
||||
.vscode
|
||||
.pytest_cache
|
||||
*.sqlite3
|
||||
venv
|
||||
28
README.md
Normal file
28
README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
### Install
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
### reset db
|
||||
|
||||
FLASK_APP=app.py FLASK_ENV=development flask reset-db
|
||||
|
||||
### create vehicles
|
||||
|
||||
FLASK_APP=app.py FLASK_ENV=development flask create-vehicles
|
||||
|
||||
### run
|
||||
|
||||
FLASK_APP=app.py FLASK_ENV=development flask run
|
||||
|
||||
### get list of vehicles
|
||||
|
||||
http://127.0.0.1:5000/vehicles
|
||||
|
||||
### Tests
|
||||
|
||||
pytest
|
||||
|
||||
### Notes for running with python 3.11
|
||||
- Updated py to 1.11.0
|
||||
- Updated pytest to 6.2.5
|
||||
- Moved `yield client` into `with app.app_context()` block in `test/__init__.py`
|
||||
37
app/__init__.py
Normal file
37
app/__init__.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from flask import Flask
|
||||
from test.vehicle_data import vehicle_data
|
||||
|
||||
from .extensions import db, ma
|
||||
from .models.vehicle import Vehicle
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite3'
|
||||
|
||||
db.init_app(app)
|
||||
ma.init_app(app)
|
||||
|
||||
@app.cli.command("reset-db")
|
||||
def reset_db():
|
||||
db.drop_all()
|
||||
db.create_all()
|
||||
|
||||
|
||||
@app.cli.command("create-vehicles")
|
||||
def create_vehicles():
|
||||
for vehicle in vehicle_data:
|
||||
db.session.add(Vehicle(**vehicle))
|
||||
db.session.commit()
|
||||
|
||||
from .routes.vehicle import vehicle_routes
|
||||
app.register_blueprint(vehicle_routes)
|
||||
|
||||
from .routes.shift import shift_routes
|
||||
app.register_blueprint(shift_routes)
|
||||
|
||||
from .routes.auto import auto_routes
|
||||
app.register_blueprint(auto_routes)
|
||||
|
||||
return app
|
||||
|
||||
5
app/extensions.py
Normal file
5
app/extensions.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_marshmallow import Marshmallow
|
||||
|
||||
db = SQLAlchemy()
|
||||
ma = Marshmallow()
|
||||
0
app/models/__init__.py
Normal file
0
app/models/__init__.py
Normal file
16
app/models/battery_change.py
Normal file
16
app/models/battery_change.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from app.extensions import db, ma
|
||||
|
||||
class BatteryChange(db.Model):
|
||||
order = db.Column(db.Integer)
|
||||
shift_id = db.Column(db.Integer, db.ForeignKey('shift.id'), primary_key=True, nullable=False)
|
||||
vehicle_id = db.Column(db.Integer, db.ForeignKey('vehicle.id'), primary_key=True, nullable=False)
|
||||
completed = db.Column(db.Boolean, nullable=False, default=False)
|
||||
vehicle = db.relationship('Vehicle', backref='change_to_vehicle')
|
||||
|
||||
class BatteryChangeSchema(ma.Schema):
|
||||
vehicle = ma.Nested('VehicleSchema', only=['license_plate', 'battery_level',
|
||||
'in_use', 'model', 'location_lat',
|
||||
'location_long', 'id'], many=False)
|
||||
class Meta:
|
||||
model=BatteryChange
|
||||
fields = ('shift_id', 'vehicle_id', 'completed', 'vehicle')
|
||||
19
app/models/shift.py
Normal file
19
app/models/shift.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from app.extensions import db, ma
|
||||
from flask_marshmallow import fields
|
||||
|
||||
class Shift(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
battery_changes = db.relationship('BatteryChange',
|
||||
backref='battery_changes',
|
||||
order_by='BatteryChange.order')
|
||||
|
||||
class ShiftSchema(ma.Schema):
|
||||
battery_changes = ma.Nested('BatteryChangeSchema', many=True)
|
||||
all_completed = fields.fields.Method('get_all_completed')
|
||||
class Meta:
|
||||
model=Shift
|
||||
include_fk=True
|
||||
fields = ("id", "battery_changes", "all_completed")
|
||||
|
||||
def get_all_completed(self, obj):
|
||||
return all(change.completed == True for change in obj.battery_changes)
|
||||
23
app/models/vehicle.py
Normal file
23
app/models/vehicle.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from app.extensions import db, ma
|
||||
|
||||
class Vehicle(db.Model):
|
||||
__tablename__ = 'vehicle'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
license_plate = db.Column(db.String(80), unique=True, nullable=False)
|
||||
battery_level = db.Column(db.Float, nullable=False)
|
||||
in_use = db.Column(db.Boolean, nullable=False)
|
||||
model = db.Column(db.String(10), nullable=False)
|
||||
location_lat = db.Column(db.Float, nullable=False)
|
||||
location_long = db.Column(db.Float, nullable=False)
|
||||
battery_change_shifts = db.relationship('BatteryChange', backref='battery_change_shifts')
|
||||
shifts = db.relationship('Shift', secondary="battery_change", backref='vehicles_to_shifts')
|
||||
|
||||
class VehicleSchema(ma.Schema):
|
||||
battery_change_shifts = ma.Nested('BatteryChangeSchema', many=True)
|
||||
shifts = ma.Nested('ShiftSchema', only=['id'], many=True)
|
||||
class Meta:
|
||||
# Fields to expose
|
||||
model = Vehicle
|
||||
fields = ("id", "license_plate", "battery_level", "model",
|
||||
"in_use", "location_lat", "location_long", 'battery_change_shifts',
|
||||
"shifts")
|
||||
0
app/routes/__init__.py
Normal file
0
app/routes/__init__.py
Normal file
145
app/routes/auto.py
Normal file
145
app/routes/auto.py
Normal file
@@ -0,0 +1,145 @@
|
||||
from flask import Blueprint
|
||||
from itertools import permutations
|
||||
from app.extensions import db
|
||||
from app.models.vehicle import Vehicle
|
||||
from app.models.shift import Shift, ShiftSchema
|
||||
from app.models.battery_change import BatteryChange
|
||||
from math import sqrt, pow
|
||||
|
||||
auto_routes = Blueprint('auto_routes', __name__)
|
||||
|
||||
@auto_routes.route('/auto/<lat>/<long>')
|
||||
def generate_auto_shift(lat, long):
|
||||
lat = float(lat)
|
||||
long = float(long)
|
||||
closest_vehicles = get_closest_vehicles(lat, long, 20)
|
||||
|
||||
solution = kruskals(closest_vehicles)
|
||||
|
||||
order = get_final_order(solution, closest_vehicles)
|
||||
|
||||
new_shift = Shift()
|
||||
for i, ind in enumerate(order):
|
||||
id = closest_vehicles[ind].id
|
||||
new_change = BatteryChange(**{
|
||||
"order": i,
|
||||
"shift_id": new_shift.id,
|
||||
"vehicle_id": id
|
||||
})
|
||||
new_shift.battery_changes.append(new_change)
|
||||
db.session.add(new_shift)
|
||||
db.session.commit()
|
||||
|
||||
shift_schema = ShiftSchema(many=False)
|
||||
|
||||
return shift_schema.dumps(new_shift)
|
||||
|
||||
def get_closest_vehicles(lat, long, num):
|
||||
vehicles = Vehicle.query.all()
|
||||
distances = []
|
||||
for vehicle in vehicles:
|
||||
distance = sqrt(pow(abs(lat) - abs(vehicle.location_lat), 2) + pow(abs(long) - abs(vehicle.location_long), 2))
|
||||
distances.append((vehicle, distance))
|
||||
|
||||
distances = sorted(distances, key=lambda x: x[1])[:num]
|
||||
return [x[0] for x in distances]
|
||||
|
||||
def get_distance_between_vehicles(left, right):
|
||||
# Assumes all lats/longs of cars to be consistently positive or negatively.
|
||||
# Would most likely include data point for city/region and only query cars in that area
|
||||
return sqrt(pow(abs(left.location_lat) - abs(right.location_lat), 2) + pow(abs(left.location_long) - abs(right.location_long), 2))
|
||||
|
||||
def get_sorted_edges(vehicles):
|
||||
'''
|
||||
Returns list of tuples representing edges (<source_node>, <other_node>, <distance>)
|
||||
where node is index of vehicle in sorted vehicles by distance from starting point.
|
||||
The return list is sorted by edge distance.
|
||||
'''
|
||||
if not vehicles or len(vehicles) == 1: return []
|
||||
|
||||
edge_list = []
|
||||
for source_ind, vehicle in enumerate(vehicles):
|
||||
for other_ind, other_vehicle in enumerate(vehicles):
|
||||
# So we don't double count edges
|
||||
if source_ind < other_ind:
|
||||
distance = get_distance_between_vehicles(vehicle, other_vehicle)
|
||||
edge_list.append((source_ind, other_ind, distance))
|
||||
|
||||
return sorted(edge_list, key = lambda edge: edge[2])
|
||||
|
||||
def kruskals(vehicles):
|
||||
'''
|
||||
Gets the optimal edges to reduce the distance travelled between nodes.
|
||||
Returns list of lists where each index represents a vehicle in the closest
|
||||
vehicle list (indexes match) and each sublist represents the edges
|
||||
to other nodes
|
||||
'''
|
||||
edges = get_sorted_edges(vehicles)
|
||||
|
||||
parents = [i for i in range(len(vehicles))]
|
||||
ranks = [0 for _ in range(len(vehicles))]
|
||||
solution = [[] for _ in range(len(vehicles))]
|
||||
|
||||
def find(vertex, parents):
|
||||
if vertex != parents[vertex]:
|
||||
parents[vertex] = find(parents[vertex], parents)
|
||||
|
||||
return parents[vertex]
|
||||
|
||||
def union(root1, root2, parents, ranks):
|
||||
if ranks[root1] < ranks[root2]:
|
||||
parents[root1] = root2
|
||||
elif ranks[root1] > ranks[root2]:
|
||||
parents[root2] = root1
|
||||
else:
|
||||
parents[root2] = root1
|
||||
ranks[root1] += 1
|
||||
|
||||
for edge in edges:
|
||||
root1 = find(edge[0], parents)
|
||||
root2 = find(edge[1], parents)
|
||||
if root1 != root2:
|
||||
solution[edge[0]].append((edge[1], edge[2]))
|
||||
solution[edge[1]].append((edge[0], edge[2]))
|
||||
union(root1, root2, parents, ranks)
|
||||
|
||||
return solution
|
||||
|
||||
def get_final_order(solution, closest_vehicles):
|
||||
'''
|
||||
Uses the solution from kruskals algorithm to determine the final route.
|
||||
'''
|
||||
|
||||
# Find first vertex with only one edge, this will be out starting point
|
||||
# i.e. Closest leaf node
|
||||
ind = 0
|
||||
for i in range(len(solution)):
|
||||
if len(solution[i]) == 1:
|
||||
ind = i
|
||||
break
|
||||
|
||||
order = [ind]
|
||||
# Keep a stack to go back to previous nodes if we hit a dead end
|
||||
stack = []
|
||||
while len(order) != len(closest_vehicles):
|
||||
while len(solution[ind]) != 0:
|
||||
# Iterate through edges of current node, removing once travelled
|
||||
edge = solution[ind].pop(0)
|
||||
|
||||
# New node, add to order and check that node's edges
|
||||
if edge[0] not in order:
|
||||
if solution[ind]:
|
||||
stack.append(ind)
|
||||
ind = edge[0]
|
||||
order.append(ind)
|
||||
break
|
||||
# Hit the end of edges for current node, go back
|
||||
if len(solution[ind]) == 0 and stack:
|
||||
ind = stack.pop()
|
||||
|
||||
# If we're out of edges in the current node and nowhere to go back to,
|
||||
# we must have visited all nodes, return out
|
||||
elif len(solution[ind]) == 0 and not stack:
|
||||
break
|
||||
|
||||
return order
|
||||
97
app/routes/shift.py
Normal file
97
app/routes/shift.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from flask import Blueprint, request, render_template_string
|
||||
from app.extensions import db
|
||||
from app.models.shift import Shift, ShiftSchema
|
||||
from app.models.battery_change import BatteryChange, BatteryChangeSchema
|
||||
|
||||
shift_routes = Blueprint('shift_routes', __name__)
|
||||
|
||||
@shift_routes.route('/shifts')
|
||||
def list_shifts():
|
||||
shift_schema = ShiftSchema(many=True)
|
||||
shifts = Shift.query.all()
|
||||
return shift_schema.dumps(shifts)
|
||||
|
||||
@shift_routes.route('/shifts/<id>')
|
||||
def get_shift(id):
|
||||
shift_schema = ShiftSchema(many=False)
|
||||
shift = Shift.query.get_or_404(id)
|
||||
return shift_schema.dumps(shift)
|
||||
|
||||
@shift_routes.route('/shifts', methods=['POST'])
|
||||
def create_shift():
|
||||
new_shift = Shift()
|
||||
|
||||
if request.form.get('vehicles'):
|
||||
vehicle_ids = [int(i) for i in request.form.get('vehicles').split(',')]
|
||||
for id in vehicle_ids:
|
||||
new_change = BatteryChange(**{
|
||||
"shift_id": new_shift.id,
|
||||
"vehicle_id": id
|
||||
})
|
||||
new_shift.battery_changes.append(new_change)
|
||||
db.session.add(new_shift)
|
||||
db.session.commit()
|
||||
shift_schema = ShiftSchema(many=False)
|
||||
return shift_schema.dumps(new_shift)
|
||||
|
||||
@shift_routes.route('/shifts/<id>', methods=['DELETE'])
|
||||
def delete_shift(id):
|
||||
shift = Shift.query.get_or_404(id)
|
||||
for change in shift.battery_changes:
|
||||
db.session.delete(change)
|
||||
db.session.commit()
|
||||
db.session.delete(shift)
|
||||
db.session.commit()
|
||||
return id
|
||||
|
||||
@shift_routes.route('/shifts/<shift_id>/vehicles/<vehicle_id>', methods=['GET'])
|
||||
def get_vehicle_in_shift(shift_id, vehicle_id):
|
||||
shift = Shift.query.get_or_404(shift_id)
|
||||
for change in shift.battery_changes:
|
||||
if change.vehicle_id == int(vehicle_id):
|
||||
change_schema = BatteryChangeSchema(many=False)
|
||||
return change_schema.dumps(change)
|
||||
return 'No vehicle found with id %s in shift %s' % (vehicle_id, shift_id), 404
|
||||
|
||||
@shift_routes.route('/shifts/<shift_id>/vehicles/<vehicle_id>', methods=['POST'])
|
||||
def add_vehicle_to_shift(shift_id, vehicle_id):
|
||||
shift_schema = ShiftSchema(many=False)
|
||||
shift = Shift.query.get_or_404(shift_id)
|
||||
shift.battery_changes.append(BatteryChange**{
|
||||
'shift_id': shift.id,
|
||||
'vehicle_id': vehicle_id
|
||||
})
|
||||
db.session.commit()
|
||||
return shift_schema.dumps(shift)
|
||||
|
||||
@shift_routes.route('/shifts/<shift_id>/vehicles/<vehicle_id>/completed', methods=['POST'])
|
||||
def complete_vehicle_in_shift(shift_id, vehicle_id):
|
||||
shift_schema = ShiftSchema(many=False)
|
||||
shift = Shift.query.get_or_404(shift_id)
|
||||
for change in shift.battery_changes:
|
||||
if int(change.vehicle_id) == int(vehicle_id):
|
||||
change.completed = True
|
||||
db.session.commit()
|
||||
return shift_schema.dumps(shift)
|
||||
|
||||
return 'Vehicle not found', 404
|
||||
|
||||
@shift_routes.route('/shifts/<shift_id>/vehicles/<vehicle_id>', methods=['DELETE'])
|
||||
def remove_vehicle_from_shift(shift_id, vehicle_id):
|
||||
shift_schema = ShiftSchema(many=False)
|
||||
shift = Shift.query.get_or_404(shift_id)
|
||||
delete_me = []
|
||||
keep_me = []
|
||||
for change in shift.battery_changes:
|
||||
if int(change.vehicle_id) == int(vehicle_id):
|
||||
delete_me.append(change)
|
||||
else:
|
||||
keep_me.append(change)
|
||||
|
||||
if not delete_me: return 'No vehicles found with id %s' % vehicle_id, 404
|
||||
|
||||
shift.battery_changes = keep_me
|
||||
for change in delete_me:
|
||||
db.session.delete(change)
|
||||
db.session.commit()
|
||||
return shift_schema.dumps(shift)
|
||||
54
app/routes/vehicle.py
Normal file
54
app/routes/vehicle.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from flask import Blueprint, request
|
||||
from app.extensions import db
|
||||
from app.models.vehicle import Vehicle, VehicleSchema
|
||||
|
||||
vehicle_routes = Blueprint('vehicle_routes', __name__)
|
||||
|
||||
@vehicle_routes.route('/vehicles')
|
||||
def list_vehicles():
|
||||
vehicles_schema = VehicleSchema(many=True)
|
||||
vehicles = Vehicle.query.all()
|
||||
return vehicles_schema.dumps(vehicles)
|
||||
|
||||
@vehicle_routes.route('/vehicles/<id>')
|
||||
def get_vehicle(id):
|
||||
vehicles_schema = VehicleSchema(many=False)
|
||||
vehicle = Vehicle.query.get_or_404(id)
|
||||
return vehicles_schema.dumps(vehicle)
|
||||
|
||||
@vehicle_routes.route('/vehicles', methods=['POST'])
|
||||
def create_vehicle():
|
||||
new_vehicle = Vehicle(**{
|
||||
"license_plate": request.form.get('license_plate'),
|
||||
"battery_level": request.form.get('battery_level', 100),
|
||||
"in_use": bool(request.form.get('in_use', True)),
|
||||
"model": request.form.get('model'),
|
||||
"location_lat": request.form.get('location_lat'),
|
||||
"location_long": request.form.get('location_long'),
|
||||
})
|
||||
db.session.add(new_vehicle)
|
||||
db.session.commit()
|
||||
vehicles_schema = VehicleSchema(many=False)
|
||||
|
||||
return vehicles_schema.dumps(new_vehicle)
|
||||
|
||||
@vehicle_routes.route('/vehicles/<id>', methods=['POST'])
|
||||
def update_vehicle(id):
|
||||
vehicles_schema = VehicleSchema(many=False)
|
||||
vehicle = Vehicle.query.get_or_404(id)
|
||||
vehicle.license_plate = request.form.get('license_plate', vehicle.license_plate)
|
||||
vehicle.battery_level = request.form.get('battery_level', vehicle.battery_level)
|
||||
vehicle.in_use = request.form.get('in_use', vehicle.in_use)
|
||||
vehicle.model = request.form.get('model', vehicle.model)
|
||||
vehicle.location_lat = request.form.get('location_lat', vehicle.location_lat)
|
||||
vehicle.location_long = request.form.get('location_long', vehicle.location_long)
|
||||
|
||||
db.session.commit()
|
||||
return vehicles_schema.dumps(vehicle)
|
||||
|
||||
@vehicle_routes.route('/vehicles/<id>', methods=['DELETE'])
|
||||
def delete_vehicle(id):
|
||||
vehicle = Vehicle.query.get_or_404(id)
|
||||
db.session.delete(vehicle)
|
||||
db.session.commit()
|
||||
return id
|
||||
27
requirements.txt
Normal file
27
requirements.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
atomicwrites==1.3.0
|
||||
attrs==19.2.0
|
||||
certifi==2019.6.16
|
||||
Click==7.0
|
||||
Flask==1.1.1
|
||||
flask-marshmallow==0.10.1
|
||||
Flask-SQLAlchemy==2.4.1
|
||||
importlib-metadata==0.23
|
||||
itsdangerous==1.1.0
|
||||
Jinja2==2.10.1
|
||||
MarkupSafe==1.1.1
|
||||
marshmallow==3.2.1
|
||||
marshmallow-sqlalchemy==0.19.0
|
||||
more-itertools==7.2.0
|
||||
packaging==19.2
|
||||
pipenv==2018.11.26
|
||||
pluggy==0.13.0
|
||||
py==1.11.0
|
||||
pyparsing==2.4.2
|
||||
pytest==6.2.5
|
||||
six==1.12.0
|
||||
SQLAlchemy==1.3.9
|
||||
virtualenv==16.7.5
|
||||
virtualenv-clone==0.5.3
|
||||
wcwidth==0.1.7
|
||||
Werkzeug==0.16.0
|
||||
zipp==0.6.0
|
||||
24
test/__init__.py
Normal file
24
test/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
from app import create_app, db, Vehicle
|
||||
from flask import json
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
app = create_app()
|
||||
|
||||
db_fd, app.config['DATABASE'] = tempfile.mkstemp()
|
||||
app.config['TESTING'] = True
|
||||
client = app.test_client()
|
||||
|
||||
with app.app_context():
|
||||
db.drop_all()
|
||||
db.create_all()
|
||||
|
||||
yield client
|
||||
|
||||
os.close(db_fd)
|
||||
os.unlink(app.config['DATABASE'])
|
||||
27
test/test_auto.py
Normal file
27
test/test_auto.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from flask import json
|
||||
from app.extensions import db
|
||||
from app.models.shift import Shift
|
||||
from app.models.battery_change import BatteryChange
|
||||
from test import client
|
||||
from test.utils import create_vehicles
|
||||
|
||||
def test_auto_shift_generation(client):
|
||||
create_vehicles(8)
|
||||
|
||||
rv = client.get('/auto/40.68136179/-73.996421')
|
||||
data = json.loads(rv.data)
|
||||
|
||||
print(data)
|
||||
|
||||
ids = [change['vehicle']['id'] for change in data['battery_changes']]
|
||||
assert ids == [2, 1, 8, 6, 7, 5, 4, 3]
|
||||
|
||||
def test_auto_shift_generation_all(client):
|
||||
create_vehicles()
|
||||
|
||||
rv = client.get('/auto/40.68136179/-73.996421')
|
||||
data = json.loads(rv.data)
|
||||
|
||||
ids = [change['vehicle']['id'] for change in data['battery_changes']]
|
||||
print(ids)
|
||||
assert ids == [2, 1, 8, 6, 7, 5, 15, 4, 10, 11, 12, 9, 14, 13, 3]
|
||||
BIN
test/test_auto_example.png
Normal file
BIN
test/test_auto_example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
31
test/test_auto_results.py
Normal file
31
test/test_auto_results.py
Normal file
@@ -0,0 +1,31 @@
|
||||
'''
|
||||
Result of kruskals algorithm with first 8 cars in the system.
|
||||
The array shows the raw output, where the index and first item in the tuple
|
||||
is the vehicles position in the sorted `closest_vehicles` array.
|
||||
The dictionary is the same data, just the keys and first item in of each tuple
|
||||
replaced with the vehicle_id.
|
||||
|
||||
test_auto_example.png shows the position of these vehicles as well as the starting point.
|
||||
'''
|
||||
|
||||
[
|
||||
[(1, 0.0018223418998603907), (2, 0.005142119115696336)],
|
||||
[(0, 0.0018223418998603907), (4, 0.0038434786326951562)],
|
||||
[(0, 0.005142119115696336)],
|
||||
[(6, 0.003218020664940648), (4, 0.004116640377786014), (5, 0.005380458066000523)],
|
||||
[(1, 0.0038434786326951562), (3, 0.004116640377786014)],
|
||||
[(3, 0.005380458066000523)],
|
||||
[(3, 0.003218020664940648), (7, 0.00569171626137669)],
|
||||
[(6, 0.00569171626137669)]
|
||||
]
|
||||
|
||||
vehicle_ids = {
|
||||
1: [(8, 0.0018223418998603907), (2, 0.005142119115696336)],
|
||||
8: [(1, 0.0018223418998603907), (6, 0.0038434786326951562)],
|
||||
2: [(1, 0.005142119115696336)],
|
||||
7: [(5, 0.003218020664940648), (6, 0.004116640377786014), (3, 0.005380458066000523)],
|
||||
6: [(8, 0.0038434786326951562), (7, 0.004116640377786014)],
|
||||
3: [(7, 0.005380458066000523)],
|
||||
5: [(7, 0.003218020664940648), (4, 0.00569171626137669)],
|
||||
4: [(5, 0.00569171626137669)]
|
||||
}
|
||||
207
test/test_shift.py
Normal file
207
test/test_shift.py
Normal file
@@ -0,0 +1,207 @@
|
||||
from flask import json
|
||||
from app.extensions import db
|
||||
from app.models.shift import Shift
|
||||
from app.models.battery_change import BatteryChange
|
||||
from test import client
|
||||
from test.utils import create_vehicles
|
||||
|
||||
def test_list_shifts_empty_db_empty_list(client):
|
||||
rv = client.get('/shifts')
|
||||
assert len(json.loads(rv.data)) == 0
|
||||
|
||||
def test_list_shifts_single_record(client):
|
||||
db.session.add(Shift())
|
||||
rv = client.get('/shifts')
|
||||
assert len(json.loads(rv.data)) == 1
|
||||
|
||||
def test_get_shift(client):
|
||||
db.session.add(Shift())
|
||||
db.session.add(Shift())
|
||||
db.session.commit()
|
||||
rv = client.get('/shifts')
|
||||
assert len(json.loads(rv.data)) == 2
|
||||
rv = client.get('/shifts/1')
|
||||
data = json.loads(rv.data)
|
||||
assert data['id'] == 1
|
||||
|
||||
rv = client.get('/shifts/2')
|
||||
data = json.loads(rv.data)
|
||||
assert data['id'] == 2
|
||||
|
||||
def test_create_empty_shift(client):
|
||||
client.post('/shifts')
|
||||
rv = client.get('/shifts/1')
|
||||
data = json.loads(rv.data)
|
||||
assert data['id'] == 1
|
||||
assert data['battery_changes'] == []
|
||||
|
||||
def test_create_shift_with_vehicles(client):
|
||||
create_vehicles()
|
||||
rv = client.get('/vehicles')
|
||||
assert len(json.loads(rv.data)) == 15
|
||||
|
||||
client.post('/shifts', data={
|
||||
'vehicles': '1,5,10'
|
||||
})
|
||||
rv = client.get('/shifts/1')
|
||||
data = json.loads(rv.data)
|
||||
assert data['id'] == 1
|
||||
assert len(data['battery_changes']) == 3
|
||||
assert data['battery_changes'][0]['vehicle_id'] == 1
|
||||
assert data['battery_changes'][1]['vehicle_id'] == 5
|
||||
assert data['battery_changes'][2]['vehicle_id'] == 10
|
||||
assert data['battery_changes'][0]['completed'] == False
|
||||
assert data['battery_changes'][0]['shift_id'] == 1
|
||||
|
||||
# Get vehicle data directly in shift
|
||||
assert data['battery_changes'][0]['vehicle']['license_plate'] == 'NY0001'
|
||||
assert data['battery_changes'][1]['vehicle']['location_long'] == -73.988838
|
||||
assert data['battery_changes'][2]['vehicle']['battery_level'] == 22
|
||||
|
||||
def add_vehicle_to_shift(client):
|
||||
create_vehicles()
|
||||
client.post('/shifts')
|
||||
rv = client.get('/shifts/1')
|
||||
data = json.loads(rv.data)
|
||||
assert data['id'] == 1
|
||||
assert data['battery_changes'] == []
|
||||
|
||||
rv = client.post('shifts/1/add/2')
|
||||
data = json.loads(rv.data)
|
||||
assert data['id'] == 1
|
||||
assert len(data['battery_changes']) == 1
|
||||
assert data['battery_changes'][0]['vehicle_id'] == 2
|
||||
|
||||
rv = client.post('shifts/1')
|
||||
data = json.loads(rv.data)
|
||||
assert data['id'] == 1
|
||||
assert len(data['battery_changes']) == 1
|
||||
assert data['battery_changes'][0]['vehicle_id'] == 2
|
||||
|
||||
# Shouldnt be able to add same vehicle to shift
|
||||
rv = client.post('shifts/1/vehicles/2')
|
||||
rv = client.post('shifts/1')
|
||||
data = json.loads(rv.data)
|
||||
assert data['id'] == 1
|
||||
assert len(data['battery_changes']) == 1
|
||||
assert data['battery_changes'][0]['vehicle_id'] == 2
|
||||
|
||||
def test_remove_vehicle_from_shift(client):
|
||||
create_vehicles()
|
||||
rv = client.get('/vehicles')
|
||||
assert len(json.loads(rv.data)) == 15
|
||||
|
||||
client.post('/shifts', data={
|
||||
'vehicles': '1,5,10'
|
||||
})
|
||||
|
||||
rv = client.delete('shifts/1/vehicles/5')
|
||||
data = json.loads(rv.data)
|
||||
assert data['id'] == 1
|
||||
assert len(data['battery_changes']) == 2
|
||||
assert data['battery_changes'][0]['vehicle_id'] == 1
|
||||
assert data['battery_changes'][1]['vehicle_id'] == 10
|
||||
|
||||
rv = client.get('/vehicles/10')
|
||||
data = json.loads(rv.data)
|
||||
assert len(data['shifts']) == 1
|
||||
assert data['shifts'][0]['id'] == 1
|
||||
|
||||
# negative testing
|
||||
rv = client.delete('shifts/1/vehicles/9')
|
||||
assert rv.status_code == 404
|
||||
rv = client.delete('shifts/1/vehicles/5')
|
||||
assert rv.status_code == 404
|
||||
|
||||
def test_delete_shift(client):
|
||||
create_vehicles()
|
||||
rv = client.get('/vehicles')
|
||||
assert len(json.loads(rv.data)) == 15
|
||||
|
||||
client.post('/shifts', data={
|
||||
'vehicles': '1,5,10'
|
||||
})
|
||||
|
||||
rv = client.get('/shifts/1')
|
||||
data = json.loads(rv.data)
|
||||
assert data['id'] == 1
|
||||
assert len(data['battery_changes']) == 3
|
||||
|
||||
client.delete('/shifts/1')
|
||||
|
||||
rv = client.get('/shifts')
|
||||
assert len(json.loads(rv.data)) == 0
|
||||
|
||||
changes = BatteryChange.query.all()
|
||||
assert len(changes) == 0
|
||||
|
||||
# negative testing
|
||||
rv = client.delete('/shifts/5')
|
||||
assert rv.status_code == 404
|
||||
|
||||
def test_complete_vehicle_charge(client):
|
||||
create_vehicles()
|
||||
|
||||
client.post('/shifts', data={
|
||||
'vehicles': '1,5,10'
|
||||
})
|
||||
rv = client.get('/shifts/1')
|
||||
data = json.loads(rv.data)
|
||||
assert data['id'] == 1
|
||||
assert len(data['battery_changes']) == 3
|
||||
assert data['battery_changes'][0]['completed'] == False
|
||||
assert data['battery_changes'][1]['completed'] == False
|
||||
assert data['battery_changes'][2]['completed'] == False
|
||||
|
||||
rv = client.post('/shifts/1/vehicles/5/completed')
|
||||
data = json.loads(rv.data)
|
||||
assert data['battery_changes'][0]['completed'] == False
|
||||
assert data['battery_changes'][1]['completed'] == True
|
||||
assert data['battery_changes'][2]['completed'] == False
|
||||
|
||||
# Negative testing
|
||||
rv = client.post('/shifts/2/vehicles/5/completed') # shift not found
|
||||
assert rv.status_code == 404
|
||||
rv = client.post('/shifts/1/vehicles/6/completed') # vehicle not found
|
||||
assert rv.status_code == 404
|
||||
|
||||
def test_get_vehicle_in_shift(client):
|
||||
create_vehicles()
|
||||
|
||||
client.post('/shifts', data={
|
||||
'vehicles': '1,5,10'
|
||||
})
|
||||
|
||||
rv = client.get('/shifts/1/vehicles/10')
|
||||
data = json.loads(rv.data)
|
||||
assert data['vehicle']['license_plate'] == 'NY0010'
|
||||
assert data['completed'] == False
|
||||
|
||||
rv = client.get('shifts/1/vehicles/11')
|
||||
assert rv.status_code == 404
|
||||
|
||||
def test_shift_all_completed(client):
|
||||
create_vehicles()
|
||||
|
||||
client.post('/shifts', data={
|
||||
'vehicles': '1,5,10'
|
||||
})
|
||||
|
||||
rv = client.get('/shifts/1')
|
||||
data = json.loads(rv.data)
|
||||
assert data['all_completed'] == False
|
||||
|
||||
client.post('/shifts/1/vehicles/1/completed')
|
||||
rv = client.get('/shifts/1')
|
||||
data = json.loads(rv.data)
|
||||
assert data['all_completed'] == False
|
||||
|
||||
client.post('/shifts/1/vehicles/5/completed')
|
||||
rv = client.get('/shifts/1')
|
||||
data = json.loads(rv.data)
|
||||
assert data['all_completed'] == False
|
||||
|
||||
client.post('/shifts/1/vehicles/10/completed')
|
||||
rv = client.get('/shifts/1')
|
||||
data = json.loads(rv.data)
|
||||
assert data['all_completed'] == True
|
||||
125
test/test_vehicle.py
Normal file
125
test/test_vehicle.py
Normal file
@@ -0,0 +1,125 @@
|
||||
from flask import json
|
||||
from app.extensions import db
|
||||
from app.models.vehicle import Vehicle
|
||||
from test import client
|
||||
|
||||
def test_list_vehicles_empty_db_empty_list(client):
|
||||
rv = client.get('/vehicles')
|
||||
assert len(json.loads(rv.data)) == 0
|
||||
|
||||
def test_list_vehicles_with_data(client):
|
||||
db.session.add(Vehicle(**{
|
||||
"id": 1,
|
||||
"license_plate": "NY0001",
|
||||
"battery_level": 90,
|
||||
"in_use": True,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.680245,
|
||||
"location_long": -73.996955,
|
||||
}))
|
||||
db.session.commit()
|
||||
rv = client.get('/vehicles')
|
||||
assert len(json.loads(rv.data)) == 1
|
||||
|
||||
def test_get_single_vehicle(client):
|
||||
db.session.add(Vehicle(**{
|
||||
"id": 1,
|
||||
"license_plate": "NY0002",
|
||||
"battery_level": 90,
|
||||
"in_use": True,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.680245,
|
||||
"location_long": -73.996955,
|
||||
}))
|
||||
db.session.add(Vehicle(**{
|
||||
"id": 2,
|
||||
"license_plate": "NY0003",
|
||||
"battery_level": 95,
|
||||
"in_use": True,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.680245,
|
||||
"location_long": -73.996955,
|
||||
}))
|
||||
db.session.commit()
|
||||
|
||||
rv = client.get('/vehicles')
|
||||
assert len(json.loads(rv.data)) == 2
|
||||
|
||||
rv = client.get('/vehicles/1')
|
||||
data = json.loads(rv.data)
|
||||
assert data['id'] == 1
|
||||
assert data['license_plate'] == 'NY0002'
|
||||
|
||||
def test_create_vehicles(client):
|
||||
client.post('/vehicles', data={
|
||||
"license_plate": "NY0003",
|
||||
"battery_level": 90,
|
||||
"in_use": True,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.680245,
|
||||
"location_long": -73.996955,
|
||||
})
|
||||
rv = client.get('/vehicles/1')
|
||||
data = json.loads(rv.data)
|
||||
assert data['id'] == 1
|
||||
assert data['license_plate'] == 'NY0003'
|
||||
|
||||
client.post('/vehicles', data={
|
||||
"license_plate": "NY0004",
|
||||
"battery_level": 90,
|
||||
"in_use": True,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.680245,
|
||||
"location_long": -73.996955,
|
||||
})
|
||||
rv = client.get('/vehicles/2')
|
||||
data = json.loads(rv.data)
|
||||
assert data['id'] == 2
|
||||
assert data['license_plate'] == 'NY0004'
|
||||
|
||||
def test_update_vehicle(client):
|
||||
client.post('/vehicles', data={
|
||||
"license_plate": "NY0006",
|
||||
"battery_level": 90,
|
||||
"in_use": True,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.680245,
|
||||
"location_long": -73.996955,
|
||||
})
|
||||
rv = client.get('/vehicles/1')
|
||||
data = json.loads(rv.data)
|
||||
assert data['id'] == 1
|
||||
assert data['license_plate'] == 'NY0006'
|
||||
|
||||
rv = client.post('/vehicles/1', data={
|
||||
"battery_level": 85,
|
||||
})
|
||||
data = json.loads(rv.data)
|
||||
assert data['id'] == 1
|
||||
assert data['battery_level'] == 85
|
||||
|
||||
rv = client.get('/vehicles/1')
|
||||
data = json.loads(rv.data)
|
||||
assert data['battery_level'] == 85
|
||||
|
||||
def test_delete_vehicle(client):
|
||||
client.post('/vehicles', data={
|
||||
"license_plate": "NY0007",
|
||||
"battery_level": 90,
|
||||
"in_use": True,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.680245,
|
||||
"location_long": -73.996955,
|
||||
})
|
||||
rv = client.get('/vehicles/1')
|
||||
data = json.loads(rv.data)
|
||||
assert data['id'] == 1
|
||||
assert data['license_plate'] == 'NY0007'
|
||||
|
||||
rv = client.delete('/vehicles/1')
|
||||
data = json.loads(rv.data)
|
||||
assert data == 1
|
||||
|
||||
rv = client.get('/vehicles')
|
||||
assert len(json.loads(rv.data)) == 0
|
||||
|
||||
12
test/utils.py
Normal file
12
test/utils.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from app.extensions import db
|
||||
from app.models.vehicle import Vehicle
|
||||
from .vehicle_data import vehicle_data
|
||||
|
||||
def create_vehicles(length = None):
|
||||
if length:
|
||||
for vehicle in vehicle_data[:length]:
|
||||
db.session.add(Vehicle(**vehicle))
|
||||
else:
|
||||
for vehicle in vehicle_data:
|
||||
db.session.add(Vehicle(**vehicle))
|
||||
db.session.commit()
|
||||
138
test/vehicle_data.py
Normal file
138
test/vehicle_data.py
Normal file
@@ -0,0 +1,138 @@
|
||||
|
||||
vehicle_data = [
|
||||
{
|
||||
"id": 1,
|
||||
"license_plate": "NY0001",
|
||||
"battery_level": 90,
|
||||
"in_use": True,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.680245,
|
||||
"location_long": -73.996955,
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"license_plate": "NY0002",
|
||||
"battery_level": 9,
|
||||
"in_use": False,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.684978,
|
||||
"location_long": -73.998965,
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"license_plate": "NY0003",
|
||||
"battery_level": 65,
|
||||
"in_use": False,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.683574,
|
||||
"location_long": -73.990715,
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"license_plate": "NY0004",
|
||||
"battery_level": 34,
|
||||
"in_use": False,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.67942,
|
||||
"location_long": -73.983841,
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"license_plate": "NY0005",
|
||||
"battery_level": 20,
|
||||
"in_use": False,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.676695,
|
||||
"location_long": -73.988838,
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"license_plate": "NY0006",
|
||||
"battery_level": 15,
|
||||
"in_use": False,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.675496,
|
||||
"location_long": -73.99468,
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"license_plate": "NY0007",
|
||||
"battery_level": 90,
|
||||
"in_use": False,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.678274,
|
||||
"location_long": -73.991642,
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"license_plate": "NY0008",
|
||||
"battery_level": 9,
|
||||
"in_use": False,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.678434,
|
||||
"location_long": -73.997158,
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"license_plate": "NY0009",
|
||||
"battery_level": 90,
|
||||
"in_use": False,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.683456,
|
||||
"location_long": -73.002047,
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"license_plate": "NY0010",
|
||||
"battery_level": 22,
|
||||
"in_use": True,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.677941,
|
||||
"location_long": -73.982731,
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"license_plate": "NY0011",
|
||||
"battery_level": 76,
|
||||
"in_use": False,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.673533,
|
||||
"location_long": -73.981992,
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"license_plate": "NY0012",
|
||||
"battery_level": 90,
|
||||
"in_use": False,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.668346,
|
||||
"location_long": -73.976115,
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"license_plate": "NY0013",
|
||||
"battery_level": 2,
|
||||
"in_use": False,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.669861,
|
||||
"location_long": -73.989846,
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"license_plate": "NY0014",
|
||||
"battery_level": 13,
|
||||
"in_use": False,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.673568,
|
||||
"location_long": -73.000575,
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"license_plate": "NY0015",
|
||||
"battery_level": 17,
|
||||
"in_use": False,
|
||||
"model": "Niu",
|
||||
"location_lat": 40.676001,
|
||||
"location_long": -73.987382,
|
||||
},
|
||||
]
|
||||
Reference in New Issue
Block a user