diff --git a/falconscoutcore/app.py b/falconscoutcore/app.py index 60bed4b..fcf9f44 100644 --- a/falconscoutcore/app.py +++ b/falconscoutcore/app.py @@ -51,46 +51,96 @@ def _process_data(*data: list[str], status_message_col) -> None: :param status_message_col: The Streamlit column for displaying status messages. :return: """ - data_maps = [] + quantitative_data_maps = [] + qualitative_data_maps = [] for raw_data in data: - data_labels = CONFIG["data_config"]["data_labels"] + quantitative_data_labels = CONFIG["data_config"]["quantitative_data_labels"] + qualitative_data_labels = CONFIG["data_config"]["qualitative_data_labels"] split_data = raw_data.split(CONFIG["data_config"]["delimiter"]) - if len(split_data) != len(data_labels): + if len(split_data) == len(quantitative_data_labels): + quantitative_data_maps.append( + { + field: _convert_string_to_proper_type(data) + for field, data in zip(quantitative_data_labels, split_data) + } + ) + elif len(split_data) == len(qualitative_data_labels): + qualitative_data_maps.append( + { + field: _convert_string_to_proper_type(data) + for field, data in zip(qualitative_data_labels, split_data) + } + ) + else: status_message_col.error( f"Scanned QR code from {split_data[0]} has {len(split_data)} fields " - f"while Core expected {len(data_labels)} fields." + f"while Core expected {len(quantitative_data_labels)} fields." f" The scouter is likely using an older version of FalconScout." ) return - data_maps.append({field: data for field, data in zip(data_labels, split_data)}) + if quantitative_data_maps: + with open(CONFIG["data_config"]["json_file"], "r+") as data_file: + scouting_data = load(data_file) + data_maps = [ + data_map + for data_map in quantitative_data_maps + if data_map not in scouting_data + ] + + # If some QR codes were already scanned + if len(data_maps) != len(data): + status_message_col.warning( + f"{len(data) - len(data_maps)} QR code(s) were already scanned.", + icon="🚨", + ) - with open(CONFIG["data_config"]["json_file"], "r+") as data_file: - scouting_data = load(data_file) - data_maps = [ - data_map for data_map in data_maps if data_map not in scouting_data - ] - - # If some QR codes were already scanned - if len(data_maps) != len(data): - status_message_col.warning( - f"{len(data) - len(data_maps)} QR code(s) were already scanned.", - icon="🚨", + # If all QR codes were already scanned + if len(data_maps) == 0: + return + + scouting_data.extend(data_maps) + + data_file.seek(0) + dump(scouting_data, data_file, indent=2) + data_file.truncate() + + status_message_col.success( + f"{len(data_maps)} QR code(s) successfully scanned!", icon="✅" ) - # If all QR codes were already scanned - if len(data_maps) == 0: - return + if qualitative_data_maps: + with open(CONFIG["data_config"]["qualitative_json_file"], "r+") as data_file: + scouting_data = load(data_file) + data_maps = [ + data_map + for data_map in qualitative_data_maps + if data_map not in scouting_data + ] + + # If some QR codes were already scanned + if len(data_maps) != len(data): + status_message_col.warning( + f"{len(data) - len(data_maps)} note scouting app QR code(s) were already scanned.", + icon="🚨", + ) + + # If all QR codes were already scanned + if len(data_maps) == 0: + return - scouting_data.extend(data_maps) + scouting_data.extend(data_maps) - data_file.seek(0) - dump(scouting_data, data_file, indent=2) - data_file.truncate() + data_file.seek(0) + dump(scouting_data, data_file, indent=2) + data_file.truncate() - status_message_col.success(f"{len(data_maps)} QR code(s) successfully scanned!", icon="✅") + status_message_col.success( + f"{len(data_maps)} note scouting app QR code(s) successfully scanned!", + icon="✅", + ) # Main functions @@ -114,7 +164,7 @@ def scan_qrcode(qr_code_col) -> None: if qr_codes: _process_data( *[qr_code.data.decode("utf-8") for qr_code in qr_codes], - status_message_col=qr_code_col + status_message_col=qr_code_col, ) @@ -122,7 +172,7 @@ def write_dataval_errors(data_val_col) -> None: """Writes the data validation errors contained in `errors.json` into the column.""" with ( open(CONFIG["data_config"]["error_json"]) as error_file, - open("./streamlit_components/error_component.html", "r") as component_file + open("./streamlit_components/error_component.html", "r") as component_file, ): scouting_data_errors = load(error_file) error_component = component_file.read() @@ -130,7 +180,7 @@ def write_dataval_errors(data_val_col) -> None: errors_by_match = sorted( scouting_data_errors, key=lambda error: int(error["match"][2:]), - reverse=True + reverse=True, ) with data_val_col: @@ -140,9 +190,9 @@ def write_dataval_errors(data_val_col) -> None: error_title=error["message"], error_type=error["error_type"], match_key=error["match"], - height="150px" + height="150px", ), - height=150 + height=150, ) @@ -154,9 +204,7 @@ def run_dataval(success_col) -> None: data_validator = DataValidation2023("./data_validation/config.yaml") with open(CONFIG["data_config"]["json_file"]) as scouting_data_file: - data_validator.validate_data( - load(scouting_data_file) - ) + data_validator.validate_data(load(scouting_data_file)) # Read how many errors were raised for the status message. with open(CONFIG["data_config"]["error_json"]) as error_file: @@ -164,14 +212,10 @@ def run_dataval(success_col) -> None: if amount_of_errors > 0: success_col.warning( - f"{amount_of_errors} errors were raised when validating the data.", - icon="🚨" + f"{amount_of_errors} errors were raised when validating the data.", icon="🚨" ) else: - success_col.success( - "No errors were raised when validating the data!", - icon="✅" - ) + success_col.success("No errors were raised when validating the data!", icon="✅") def sync_to_github(success_col) -> None: @@ -179,13 +223,16 @@ def sync_to_github(success_col) -> None: :param success_col: The column to write success messages into. """ - with open(CONFIG["data_config"]["json_file"]) as file: + with ( + open(CONFIG["data_config"]["json_file"]) as file, + open(CONFIG["data_config"]["qualitative_json_file"]) as qualitative_file, + ): file_json_data = load(file) + qualitative_json_data = load(qualitative_file) file_csv_data = pd.read_csv( CONFIG["data_config"].get( - "csv_file", - CONFIG["data_config"]["json_file"].replace("json", "csv") + "csv_file", CONFIG["data_config"]["json_file"].replace("json", "csv") ) ) @@ -198,6 +245,14 @@ def sync_to_github(success_col) -> None: contents.sha, ) + contents = repo.get_contents(CONFIG["repo_config"]["update_qualitative_json"]) + repo.update_file( + contents.path, + f'updated data @ {datetime.datetime.now().strftime("%m/%d/%Y, %H:%M:%S")}', + str(qualitative_json_data), + contents.sha, + ) + contents = repo.get_contents(CONFIG["repo_config"]["update_csv"]) repo.update_file( contents.path, @@ -211,22 +266,46 @@ def sync_to_github(success_col) -> None: def display_data() -> None: """Displays the scouting data in a table format that can be edited and is paginated.""" - with open(CONFIG["data_config"]["json_file"]) as data_file: + with ( + open(CONFIG["data_config"]["json_file"]) as data_file, + open(CONFIG["data_config"]["qualitative_json_file"]) as qualitative_data_file, + ): scouting_data = load(data_file) + note_scouting_data = load(qualitative_data_file) + scouting_df = pd.DataFrame.from_dict(scouting_data) + note_scouting_df = pd.DataFrame.from_dict(note_scouting_data) data_display_col, status_message_col = st.columns([1.5, 1], gap="medium") data_display_col.write("### 📊 Scouting Data Editor") status_message_col.write("### ✅ Status Messages") + # Quantitative scouting data editor resultant_df = data_display_col.data_editor(scouting_df, num_rows="dynamic") + # Qualitative scouting data editor + data_display_col.write("### 📝 Note Scouting Data Editor") + resultant_quali_df = data_display_col.data_editor( + note_scouting_df, num_rows="dynamic" + ) + # Check if the data changed - if not scouting_df[~scouting_df.apply(tuple, 1).isin(resultant_df.apply(tuple, 1))].empty: - status_message_col.success("Data changed successfully!", icon="✅") + if not scouting_df[ + ~scouting_df.apply(tuple, 1).isin(resultant_df.apply(tuple, 1)) + ].empty: + status_message_col.success("Scouting data changed successfully!", icon="✅") + resultant_df.to_json( + CONFIG["data_config"]["json_file"], orient="records", indent=2 + ) - resultant_df.to_json(CONFIG["data_config"]["json_file"], orient="records", indent=2) + if not note_scouting_df[ + ~note_scouting_df.apply(tuple, 1).isin(resultant_quali_df.apply(tuple, 1)) + ].empty: + status_message_col.success("Note scouting data changed successfully!", icon="✅") + resultant_quali_df.to_json( + CONFIG["data_config"]["qualitative_json_file"], orient="records", indent=2 + ) if __name__ == "__main__": diff --git a/falconscoutcore/config.json b/falconscoutcore/config.json index 7bbca0d..db43a08 100644 --- a/falconscoutcore/config.json +++ b/falconscoutcore/config.json @@ -1,7 +1,7 @@ { "data_config": { "delimiter": "|", - "data_labels": [ + "quantitative_data_labels": [ "ScoutId", "MatchKey", "Alliance", @@ -28,17 +28,38 @@ "DriverRating", "RatingNotes" ], - "data_types": [ - "string", - "string", - "string", - "int" + "qualitative_data_labels": [ + "ScoutId", + "MatchKey", + "Alliance", + "DriverStation", + "TeamNumber", + "AutoPieces", + "AutoStartingPosition", + "AutoEngaged", + "AutoIntakeAccuracy", + "AutoScoringAccuracy", + "AutoDrivingSkills", + "AutoNotes", + "TeleopPieces", + "TeleopPath", + "TeleopAligningSpeed", + "TeleopCommunitySkill", + "TeleopIntakingLocation", + "Disabled", + "Tippy", + "DriverRating", + "ConeIntakingSkill", + "CubeIntakingSkill", + "SubstationSpeed" ], "json_file": "./data/2023new_match_data.json", + "qualitative_json_file": "./data/2023new_qualitative_data.json", "error_json": "./data/errors.json" }, "repo_config": { "repo": "team4099/ScoutingAppData", - "update_json": "2023new_match_data.json" + "update_json": "2023new_match_data.json", + "update_qualitative_json": "2023new_qualitative_data.json" } } diff --git a/falconscoutcore/data/2023new_match_data.json b/falconscoutcore/data/2023new_match_data.json index ac3979e..5bf08e4 100644 --- a/falconscoutcore/data/2023new_match_data.json +++ b/falconscoutcore/data/2023new_match_data.json @@ -1998,7 +1998,7 @@ "ScoutId":"Nikhil", "MatchKey":"qm107", "Alliance":"blue", - "DriverStation":1, + "DriverStation":2, "TeamNumber":6431, "Preloaded":"true", "AutoGrid":"None", @@ -23169,4 +23169,4 @@ "SuccessfulTripleBalance":0, "uuid":"bceeb963-f6ea-4a86-aa7d-5065b88a7e65" } -] \ No newline at end of file +] diff --git a/falconscoutcore/data/2023new_qualitative_data.json b/falconscoutcore/data/2023new_qualitative_data.json new file mode 100644 index 0000000..d89950d --- /dev/null +++ b/falconscoutcore/data/2023new_qualitative_data.json @@ -0,0 +1,27 @@ +[ + { + "ScoutId":"shayaan", + "MatchKey":"qm,1", + "Alliance":"red", + "DriverStation":"1", + "TeamNumber":4099.0, + "AutoPieces":2.0, + "AutoStartingPosition":"Cable protector side", + "AutoEngaged":"true", + "AutoIntakeAccuracy":"false", + "AutoScoringAccuracy":"Great", + "AutoDrivingSkills":"true", + "AutoNotes":"", + "TeleopPieces":2.0, + "TeleopPath":"Cable protector side to loading zone", + "TeleopAligningSpeed":"Slow", + "TeleopCommunitySkill":"Smoothly", + "TeleopIntakingLocation":"Ground", + "Disabled":"true", + "Tippy":"true", + "DriverRating":"Fluid", + "ConeIntakingSkill":"Very Good", + "CubeIntakingSkill":"Good", + "SubstationSpeed":"Poor" + } +]