commit 1c64f16aefd5b43cc4beace3c7488b7b78621bf6 Author: Dave Dietrick Date: Thu May 18 18:21:00 2023 -0400 Initial checkin diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7322804 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +**pycache** +.vscode +.pytest_cache +*.sqlite3 +venv \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a577b61 --- /dev/null +++ b/README.md @@ -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` \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..ee1d90e --- /dev/null +++ b/app/__init__.py @@ -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 + diff --git a/app/extensions.py b/app/extensions.py new file mode 100644 index 0000000..18dc171 --- /dev/null +++ b/app/extensions.py @@ -0,0 +1,5 @@ +from flask_sqlalchemy import SQLAlchemy +from flask_marshmallow import Marshmallow + +db = SQLAlchemy() +ma = Marshmallow() \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/models/battery_change.py b/app/models/battery_change.py new file mode 100644 index 0000000..02ad151 --- /dev/null +++ b/app/models/battery_change.py @@ -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') \ No newline at end of file diff --git a/app/models/shift.py b/app/models/shift.py new file mode 100644 index 0000000..44e5e04 --- /dev/null +++ b/app/models/shift.py @@ -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) \ No newline at end of file diff --git a/app/models/vehicle.py b/app/models/vehicle.py new file mode 100644 index 0000000..cf4fcc8 --- /dev/null +++ b/app/models/vehicle.py @@ -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") \ No newline at end of file diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/auto.py b/app/routes/auto.py new file mode 100644 index 0000000..f6c5db0 --- /dev/null +++ b/app/routes/auto.py @@ -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//') +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 (, , ) + 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 \ No newline at end of file diff --git a/app/routes/shift.py b/app/routes/shift.py new file mode 100644 index 0000000..ccd103a --- /dev/null +++ b/app/routes/shift.py @@ -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/') +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/', 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//vehicles/', 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//vehicles/', 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//vehicles//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//vehicles/', 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) \ No newline at end of file diff --git a/app/routes/vehicle.py b/app/routes/vehicle.py new file mode 100644 index 0000000..23012da --- /dev/null +++ b/app/routes/vehicle.py @@ -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/') +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/', 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/', methods=['DELETE']) +def delete_vehicle(id): + vehicle = Vehicle.query.get_or_404(id) + db.session.delete(vehicle) + db.session.commit() + return id \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e487b28 --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..361786a --- /dev/null +++ b/test/__init__.py @@ -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']) \ No newline at end of file diff --git a/test/test_auto.py b/test/test_auto.py new file mode 100644 index 0000000..909fb3c --- /dev/null +++ b/test/test_auto.py @@ -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] \ No newline at end of file diff --git a/test/test_auto_example.png b/test/test_auto_example.png new file mode 100644 index 0000000..1893289 Binary files /dev/null and b/test/test_auto_example.png differ diff --git a/test/test_auto_results.py b/test/test_auto_results.py new file mode 100644 index 0000000..4887869 --- /dev/null +++ b/test/test_auto_results.py @@ -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)] +} \ No newline at end of file diff --git a/test/test_shift.py b/test/test_shift.py new file mode 100644 index 0000000..2ce1130 --- /dev/null +++ b/test/test_shift.py @@ -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 diff --git a/test/test_vehicle.py b/test/test_vehicle.py new file mode 100644 index 0000000..ec6dbfc --- /dev/null +++ b/test/test_vehicle.py @@ -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 + diff --git a/test/utils.py b/test/utils.py new file mode 100644 index 0000000..a5a301e --- /dev/null +++ b/test/utils.py @@ -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() \ No newline at end of file diff --git a/test/vehicle_data.py b/test/vehicle_data.py new file mode 100644 index 0000000..ab25583 --- /dev/null +++ b/test/vehicle_data.py @@ -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, + }, +]