From a626fe1a19b4f084ff6b17ed2c9e22c76fa39fff Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 1 Jun 2023 07:00:10 +0200 Subject: [PATCH] ft: add frontend --- backend/api/v1/__init__.py | 3 - backend/api/v1/helpers.py | 2 - backend/api/v1/inputs/inputs.py | 77 +++++++++++++++++ backend/api/v1/validations.py | 61 ++++++++++++++ backend/api/v1/views/batteries.py | 15 +++- backend/api/v1/views/drivers.py | 34 ++++++-- backend/api/v1/views/movements.py | 11 +++ backend/api/v1/views/stations.py | 31 +++++-- backend/api/v1/views/swaps.py | 16 +++- backend/config.py | 4 +- frontend/index.html | 2 +- frontend/package.json | 1 + frontend/src/configs/helpers.js | 8 ++ frontend/src/index.jsx | 4 + frontend/src/pages/Battery/BatteriesPage.jsx | 19 ++--- .../BatteryMovement/BatteryMovementPage.jsx | 47 ----------- frontend/src/pages/Driver/DriversPage.jsx | 10 +-- frontend/src/pages/Login/LoginPage.jsx | 19 +++-- frontend/src/pages/Station/StationsPage.jsx | 8 +- .../StationBattery/StationBatteryPage.jsx | 83 ++++++++----------- .../StationSwapHistoryPage.jsx | 16 ++-- .../StationBatteryMovementPage.jsx | 41 ++------- .../src/pages/SwapHistory/SwapHistoryPage.jsx | 15 +--- frontend/src/redux/features/apiSlice.js | 2 +- frontend/yarn.lock | 9 +- 25 files changed, 334 insertions(+), 204 deletions(-) create mode 100644 backend/api/v1/inputs/inputs.py create mode 100644 backend/api/v1/validations.py diff --git a/backend/api/v1/__init__.py b/backend/api/v1/__init__.py index 07ecf68..3a7f240 100644 --- a/backend/api/v1/__init__.py +++ b/backend/api/v1/__init__.py @@ -1,6 +1,3 @@ -''' - Our Main api routes -''' from functools import wraps from flask import jsonify, request diff --git a/backend/api/v1/helpers.py b/backend/api/v1/helpers.py index 8824371..38827b6 100644 --- a/backend/api/v1/helpers.py +++ b/backend/api/v1/helpers.py @@ -48,10 +48,8 @@ def token_info(token): claims = jwt.decode(token.split(' ')[1], 'test') claims.validate() except ExpiredTokenError as e: - print("=>", e) return False except Exception as e: # noqa: E722 - print("===>", e) return False return claims diff --git a/backend/api/v1/inputs/inputs.py b/backend/api/v1/inputs/inputs.py new file mode 100644 index 0000000..be3f12c --- /dev/null +++ b/backend/api/v1/inputs/inputs.py @@ -0,0 +1,77 @@ +''' Input Validation Classes ''' +from api.v1.validations import Validations + + +# Registration validations +REGISTER_DRIVER = [ + {'name': [('string', True), ('minimum', 1), + ('maximum', 30), ('required', True)]}, + {'email': [('minimum', 6), ('maximum', 30), + ('required', True), ('email', True)]}, + {'phone': [('minimum', 8), ('maximum', 10), ('required', True)]}, + {'address': [('minimum', 6), ('maximum', 30)]}, + {'license_number': [('minimum', 6), ('maximum', 30)]}, + {'license_expiry': [('minimum', 6), ('maximum', 30)]}, + {'motocycle_make': [('minimum', 6), ('maximum', 30)]}, + {'motocycle_model': [('minimum', 6), ('maximum', 30)]}, + {'motocycle_year': [('minimum', 2), ('maximum', 4)]}, +] +# Login validation +LOGIN_RULES = [ + {'email': [('minimum', 6), ('maximum', 30), + ('required', True), ('email', True)]}, + {'password': [('minimum', 6), ('required', True)]}, + {'station': [('minimum', 1)]} +] +# Change password validations +REGISTER_STATION = [ + {'name': [('minimum', 3), ('maximum', 30), ('required', True)]}, + {'location': [('minimum', 3), ('maximum', 30), ('required', True)]}, +] +# Reset password validations +REGISTER_BATTERY_RULES = [ + {'battery_type': [('minimum', 3), ('maximum', 14), + ('required', True)]}, + {'manufacture_date': [('minimum', 2), ('maximum', 4), ('required', True)]}, + {'serial_number': [('minimum', 5), ('maximum', 30), + ('required', True)]}, + {'station': [('minimum', 1), ('maximum',10), + ('required', True)]}, +] + +# Reset password validations +REGISTER_SWAP_RULE = [ + {'battery': [('required', True), ('minimum', 1), + ('maximum', 10)]}, + {'driver': [('required', True), ('minimum', 1), + ('maximum', 10)]} +] + +REGISTER_MOVEMENT_RULE = [ + {'lat': [('required', True)]}, + {'long': [('required', True)]}, + {'battery_percentage': [('required', True), ('minimum', 1), + ('maximum', 2)]} +] + + +def validate(inputs, all_rules): + ''' Register validation method ''' + error_bag = {} + valid = Validations(inputs) + for rules in all_rules: + for key in rules: + rule_key = key + for rule in rules[rule_key]: + execute = getattr(valid, rule[0])(rule_key, rule[1]) + if execute is True: + pass + if execute is not True: + if rule_key in error_bag: + error_bag[rule_key].append(execute) + else: + error_bag[rule_key] = [] + error_bag[rule_key].append(execute) + if len(error_bag) is not 0: + return error_bag + return True diff --git a/backend/api/v1/validations.py b/backend/api/v1/validations.py new file mode 100644 index 0000000..62e32f6 --- /dev/null +++ b/backend/api/v1/validations.py @@ -0,0 +1,61 @@ +''' +Validations Methods Class +''' + +import re + + +class Validations(): + '''Validations class''' + + def __init__(self, all_inputs): + ''' All inputs dictionary should be available to the class''' + for key, value in all_inputs.items(): + if (all_inputs[key] is not None and + not isinstance(all_inputs[key], int)): + if str(all_inputs[key]).strip() == '': + all_inputs[key] = None + self.all = all_inputs + + def string(self, key, string): + '''Check if input is required''' + if key in self.all and self.all[key] is not None: + if not re.match(r"[^[a-zA-Z0-9]+$", self.all[key]): + return True + return key.capitalize() + " should be string" + return True + + def minimum(self, key, minimum): + '''Check required character size''' + if key in self.all and self.all[key] is not None: + if len(str(self.all[key])) < int(minimum): + return "{} should not be less than {} characters".format( + key.capitalize(), str(minimum)) + return True + return True + + def maximum(self, key, maximum): + '''Check required character size''' + if key in self.all and self.all[key] is not None: + if len(str(self.all[key])) > int(maximum): + return "{} should not be greater than {} characters".format( + key.capitalize(), str(maximum) + ) + return True + return True + + def email(self, key, email): + '''Check required character size''' + if key in self.all and self.all[key] is not None: + if not re.match(r"[^@\s]+@[^@\s]+\.[a-zA-Z]+$", self.all[key]): + return "Invalid email address" + return True + return True + + def required(self, key, is_required=True): + '''Check input it is required''' + if key in self.all: + if self.all[key] is None or str(self.all[key]).strip() == '': + return key.capitalize() + " should not be empty" + return True + return key.capitalize() + " is required" diff --git a/backend/api/v1/views/batteries.py b/backend/api/v1/views/batteries.py index 230fd19..0c52e56 100644 --- a/backend/api/v1/views/batteries.py +++ b/backend/api/v1/views/batteries.py @@ -1,18 +1,27 @@ from flask import request, jsonify from api.v1.views import app_views from api.v1.models.battery import Battery +from api.v1.inputs.inputs import REGISTER_BATTERY_RULES, validate @app_views.route('batteries/addbattery', methods=['POST'], strict_slashes=False) def create_battery(): try: sent_data = request.get_json() - print(sent_data) + + valid = validate(sent_data, REGISTER_BATTERY_RULES) + + if valid is not True: + return jsonify( + status='error', + message="Please provide valid details", + errors=valid),400 + resp = Battery.save({ 'battery_type': sent_data['battery_type'], 'manufacture_date': sent_data['manufacture_date'], 'serial_number': sent_data['serial_number'], - 'station_id': sent_data['station_id'] + 'station_id': sent_data['station'] }) return jsonify({'status': 'Ok', 'message': 'Registered new battery', @@ -20,7 +29,7 @@ def create_battery(): except Exception as e: return jsonify({ 'status': "Error", - "message": "Error adding a battery: {}".format(e) + "message": "Error adding a battery: {}".format(e) }), 400 diff --git a/backend/api/v1/views/drivers.py b/backend/api/v1/views/drivers.py index 58ea155..bf993bb 100644 --- a/backend/api/v1/views/drivers.py +++ b/backend/api/v1/views/drivers.py @@ -7,6 +7,8 @@ from api.v1 import auth from sqlalchemy import desc, func +from api.v1.inputs.inputs import LOGIN_RULES, REGISTER_DRIVER, validate + creds = { "admin@example.com": { @@ -27,15 +29,24 @@ @app_views.route("/login", methods=["POST"], strict_slashes=False) def login(): - sent_data = request.get_json() + sent_data = request.get_json(force=True) + + valid = validate(sent_data, LOGIN_RULES) + + if valid is not True: + return jsonify( + status='error', + message="Please provide valid details", + errors=valid), 400 + email = sent_data["email"] password = sent_data["password"] - station_id = sent_data.get("station_id", None) + station = sent_data.get("station", None) if email in creds and password == creds[email]["password"]: # check if he is an admin - if station_id is None and creds[email]["role"] == "admin": + if station is None and creds[email]["role"] == "admin": # Generate JWT token token = get_token(creds[email]) @@ -43,9 +54,9 @@ def login(): return jsonify({"email": email, "token": token}) # check if a user is a manager - elif station_id is not None and creds[email]["role"] == "manager": + elif station is not None and creds[email]["role"] == "manager": # append the station_id to the user obj - creds[email]["station_id"] = station_id + creds[email]["station_id"] = station # Generate JWT token token = get_token(creds[email]) @@ -61,7 +72,16 @@ def login(): @app_views.route("drivers/addriver", methods=["POST"], strict_slashes=False) def create_driver(): try: - sent_data = request.get_json() + sent_data = request.get_json(force=True) + + valid = validate(sent_data, REGISTER_DRIVER) + + if valid is not True: + return jsonify( + status='error', + message="Please provide valid details", + errors=valid), 400 + data = { "name": sent_data.get("name"), "email": sent_data.get("email"), @@ -83,7 +103,7 @@ def create_driver(): return jsonify({ "status": "error", "message":( - "You have already " "registered driver with the same name"), + "You have alreadyregistered driver with the same name"), }), 400 resp = Driver.save(data) diff --git a/backend/api/v1/views/movements.py b/backend/api/v1/views/movements.py index eb560b0..5a1c36b 100644 --- a/backend/api/v1/views/movements.py +++ b/backend/api/v1/views/movements.py @@ -5,11 +5,22 @@ from api.v1.models.swap import Swap from sqlalchemy import and_ +from api.v1.inputs.inputs import REGISTER_MOVEMENT_RULE, validate + @app_views.route('movements/', methods=['POST'], strict_slashes=False) def update_movement(serial_number): try: sent_data = request.get_json() + + valid = validate(sent_data, REGISTER_MOVEMENT_RULE) + + if valid is not True: + return jsonify( + status='error', + message="Please provide valid details", + errors=valid),400 + battery_check = Battery.query.filter( Battery.serial_number == serial_number).first() if battery_check is None: diff --git a/backend/api/v1/views/stations.py b/backend/api/v1/views/stations.py index b627d33..4ee3bd9 100644 --- a/backend/api/v1/views/stations.py +++ b/backend/api/v1/views/stations.py @@ -4,22 +4,41 @@ from api.v1.models.station import Station from api.v1.views import app_views from api.v1.models.swap import Swap +from api.v1.inputs.inputs import REGISTER_STATION, validate +from api.v1 import auth + @app_views.route('stations/addstation', methods=['POST'], strict_slashes=False) def create_station(): try: - sent_data = request.get_json() + sent_data = request.get_json(force=True) + + valid = validate(sent_data, REGISTER_STATION) + + if valid is not True: + response = jsonify( + status='error', + message="Please provide valid details", + errors=valid) + response.status_code = 400 + return response + + resp = Station.save({'name': sent_data['name'], 'location': sent_data['location']}) - return jsonify({'status': 'Ok', 'message': 'New Station registered', 'data': resp.serialize_one}) + return jsonify({'status': 'Ok', + 'message': 'New Station registered', + 'data': resp.serialize_one}) except Exception as e: - return jsonify({ - 'status': "Error", - "message": "Error creating a station: {}".format(e) - }), 400 + return jsonify( + status = "Error", + message = "Error creating a station: {}".format(e) + ), 400 + +@auth @app_views.route("/stations", methods=["GET"], strict_slashes=False) def get_stations(): try: diff --git a/backend/api/v1/views/swaps.py b/backend/api/v1/views/swaps.py index dbf33fc..9d07145 100644 --- a/backend/api/v1/views/swaps.py +++ b/backend/api/v1/views/swaps.py @@ -2,16 +2,26 @@ from api.v1.models.swap import Swap from api.v1.views import app_views from api.v1.helpers import measurePath, token_info +from api.v1.inputs.inputs import REGISTER_SWAP_RULE, validate @app_views.route("swaps/addswap", methods=["POST"], strict_slashes=False) def create_battery_swap(): try: sent_data = request.get_json() + + valid = validate(sent_data, REGISTER_SWAP_RULE) + + if valid is not True: + return jsonify( + status='error', + message="Please provide valid details", + errors=valid),400 + user_info = token_info(request.headers.get("Authorization")) check_battery = ( Swap.query - .filter(Swap.battery_id == sent_data["battery_id"]) + .filter(Swap.battery_id == sent_data["battery"]) .filter(Swap.end_time == None) .first() ) @@ -36,8 +46,8 @@ def create_battery_swap(): swap = Swap.save( { - "battery_id": sent_data["battery_id"], - "driver_id": sent_data["driver_id"], + "battery_id": sent_data["battery"], + "driver_id": sent_data["driver"], "station_id": user_info["station_id"], } ) diff --git a/backend/config.py b/backend/config.py index d8fff35..827c6da 100644 --- a/backend/config.py +++ b/backend/config.py @@ -20,8 +20,8 @@ class ProductionConfig(Config): DEBUG = False TESTING = False # SQLAlchemy Config - # SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URI') - SQLALCHEMY_DATABASE_URI = "mysql://root:root@batteryswap-db:3308/energize_swap_db" + SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URI') + # SQLALCHEMY_DATABASE_URI = "mysql://root:root@batteryswap-db:3308/energize_swap_db" SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_ECHO = True SQLALCHEMY_POOL_TIMEOUT = 10 diff --git a/frontend/index.html b/frontend/index.html index 1fefb4f..bbfa1ae 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -35,7 +35,7 @@
- +