diff --git a/README.md b/README.md index bc6d120..8eedf83 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,57 @@ Commands: validate ``` +## redcap2reproschema Usage +The `redcap2reproschema` function is designed to process a given REDCap CSV file and YAML configuration to generate the output in the reproschema format. + +### Prerequisites +Before the conversion, ensure you have the following: + +**YAML Configuration File**: + - Download [templates/redcap2rs.yaml](templates/redcap2rs.yaml) and fill it out with your protocol details. + +### YAML File Configuration +In the `templates/redcap2rs.yaml` file, provide the following information: + +- **protocol_name**: This is a unique identifier for your protocol. Use underscores for spaces and avoid special characters. +- **protocol_display_name**: The name that will appear in the application. +- **protocol_description**: A brief description of your protocol. + +Example: +```yaml +protocol_name: "My_Protocol" +protocol_display_name: "Assessment Protocol" +protocol_description: "This protocol is for assessing cognitive skills." +``` +### Command-Line Usage + +The `redcap2reproschema`` function has been integrated into a CLI tool, use the following command: +```bash +reproschema redcap2reproschema path/to/your_redcap_data_dic.csv path/to/your_redcap2rs.yaml +``` + +### Python Function Usage + +You can also use the `redcap2reproschema` function from the `reproschema-py` package in your Python code. + +```python +from reproschema import redcap2reproschema + +csv_path = "path-to/your_redcap_data_dic.csv" +yaml_path = "path-to/your_redcap2rs.yaml" + +reproschema2redcap(input_dir_path, output_csv_filename) +``` + +After configuring the YAML file: + +1. Run the Python script with the paths to your CSV file and the YAML file as arguments. +2. Command Format: `python script_name.py path/to/your_redcap_data_dic.csv path/to/your_redcap2rs.yaml` + +### Notes +1. The script requires an active internet connection to access the GitHub repository. +2. Make sure you use `git add`, `git commit`, `git push` properly afterwards to maintain a good version control for your converted data. + ## Developer installation Install repo in developer mode: diff --git a/reproschema/cli.py b/reproschema/cli.py index adbf509..317d64f 100644 --- a/reproschema/cli.py +++ b/reproschema/cli.py @@ -3,6 +3,7 @@ from . import get_logger, set_logger_level from . import __version__ +from .redcap2reproschema import redcap2reproschema as redcap2rs lgr = get_logger() @@ -95,3 +96,17 @@ def serve(port): from .utils import start_server start_server(port=port) + + +@main.command() +@click.argument("csv_path", type=click.Path(exists=True, dir_okay=False)) +@click.argument("yaml_path", type=click.Path(exists=True, dir_okay=False)) +def redcap2reproschema(csv_path, yaml_path): + """ + Convert REDCap CSV files to Reproschema format. + """ + try: + redcap2rs(csv_path, yaml_path) + click.echo("Converted REDCap data dictionary to Reproschema format.") + except Exception as e: + raise click.ClickException(f"Error during conversion: {e}") diff --git a/reproschema/redcap2reproschema.py b/reproschema/redcap2reproschema.py new file mode 100644 index 0000000..16b80c7 --- /dev/null +++ b/reproschema/redcap2reproschema.py @@ -0,0 +1,492 @@ +import os +import argparse +import csv +import json +import re +import yaml +from bs4 import BeautifulSoup + + +def normalize_condition(condition_str): + re_parentheses = re.compile(r"\(([0-9]*)\)") + re_non_gt_lt_equal = re.compile(r"([^>|<])=") + re_brackets = re.compile(r"\[([^\]]*)\]") + + condition_str = re_parentheses.sub(r"___\1", condition_str) + condition_str = re_non_gt_lt_equal.sub(r"\1 ==", condition_str) + condition_str = condition_str.replace(" and ", " && ").replace(" or ", " || ") + condition_str = re_brackets.sub(r" \1 ", condition_str) + return condition_str + + +def process_visibility(data): + condition = data.get("Branching Logic (Show field only if...)") + if condition: + condition = normalize_condition(condition) + else: + condition = True + + visibility_obj = { + "variableName": data["Variable / Field Name"], + "isAbout": f"items/{data['Variable / Field Name']}", + "isVis": condition, + } + return visibility_obj + + +def parse_field_type_and_value(data, input_type_map): + field_type = data.get("Field Type", "") + + input_type = input_type_map.get(field_type, field_type) + + value_type_map = { + "number": "xsd:int", + "date_": "xsd:date", + "datetime_": "datetime", + "time_": "xsd:date", + "email": "email", + "phone": "phone", + } + validation_type = data.get("Text Validation Type OR Show Slider Number", "") + + value_type = value_type_map.get(validation_type, "xsd:string") + + return input_type, value_type + + +def process_choices(choices_str): + choices = [] + for choice in choices_str.split("|"): + parts = choice.split(", ") + choice_obj = {"schema:value": int(parts[0]), "schema:name": parts[1]} + if len(parts) == 3: + # TODO: handle image url + choice_obj["schema:image"] = f"{parts[2]}.png" + choices.append(choice_obj) + return choices + + +def write_to_file(abs_folder_path, form_name, field_name, rowData): + file_path = os.path.join( + f"{abs_folder_path}", "activities", form_name, "items", f"{field_name}" + ) + os.makedirs(os.path.dirname(file_path), exist_ok=True) + try: + with open(file_path, "w") as file: + json.dump(rowData, file, indent=4) + print(f"Item schema for {form_name} written successfully.") + except Exception as e: + print(f"Error in writing item schema for {form_name}: {e}") + + +def parse_html(input_string, default_language="en"): + result = {} + soup = BeautifulSoup(input_string, "html.parser") + + lang_elements = soup.find_all(True, {"lang": True}) + if lang_elements: + for element in lang_elements: + lang = element.get("lang", default_language) + text = element.get_text(strip=True) + if text: + result[lang] = text + if not result: + result[default_language] = soup.get_text(strip=True) + else: + result[default_language] = input_string + + return result + + +def process_row( + abs_folder_path, + schema_context_url, + form_name, + field, + schema_map, + input_type_map, + ui_list, + response_list, + additional_notes_list, +): + rowData = { + "@context": schema_context_url, + "@type": "reproschema:Field", + } + + field_type = field.get("Field Type", "") + schema_map["Choices, Calculations, OR Slider Labels"] = ( + "scoringLogic" if field_type == "calc" else "choices" + ) + + input_type, value_type = parse_field_type_and_value(field, input_type_map) + rowData["ui"] = {"inputType": input_type} + if value_type: + rowData["responseOptions"] = {"valueType": value_type} + + for key, value in field.items(): + if schema_map.get(key) == "allow" and value: + rowData.setdefault("ui", {}).update({schema_map[key]: value.split(", ")}) + + elif key in ui_list and value: + rowData.setdefault("ui", {}).update( + {schema_map[key]: input_type_map.get(value, value)} + ) + + elif key in response_list and value: + if key == "multipleChoice": + value = value == "1" + rowData.setdefault("responseOptions", {}).update({schema_map[key]: value}) + + elif schema_map.get(key) == "choices" and value: + rowData.setdefault("responseOptions", {}).update( + {"choices": process_choices(value)} + ) + + elif schema_map.get(key) == "scoringLogic" and value: + condition = normalize_condition(value) + rowData.setdefault("ui", {}).update({"hidden": True}) + rowData.setdefault("scoringLogic", []).append( + { + "variableName": field["Variable / Field Name"], + "jsExpression": condition, + } + ) + + elif schema_map.get(key) == "visibility" and value: + condition = normalize_condition(value) + rowData.setdefault("visibility", []).append( + {"variableName": field["Variable / Field Name"], "isVis": condition} + ) + + elif key in ["question", "schema:description", "preamble"] and value: + rowData.update({schema_map[key]: parse_html(value)}) + + elif key == "Identifier?" and value: + identifier_val = value.lower() == "y" + rowData.update( + { + schema_map[key]: [ + {"legalStandard": "unknown", "isIdentifier": identifier_val} + ] + } + ) + + elif key in additional_notes_list and value: + notes_obj = {"source": "redcap", "column": key, "value": value} + rowData.setdefault("additionalNotesObj", []).append(notes_obj) + + write_to_file(abs_folder_path, form_name, field["Variable / Field Name"], rowData) + + +def create_form_schema( + abs_folder_path, + schema_context_url, + form_name, + activity_display_name, + activity_description, + order, + bl_list, + matrix_list, + scores_list, +): + # Construct the JSON-LD structure + json_ld = { + "@context": schema_context_url, + "@type": "reproschema:Activity", + "@id": f"{form_name}_schema", + "prefLabel": activity_display_name, + "description": activity_description, + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "order": order.get(form_name, []), + "addProperties": bl_list, + "shuffle": False, + }, + } + + if matrix_list: + json_ld["matrixInfo"] = matrix_list + if scores_list: + json_ld["scoringLogic"] = scores_list + + path = os.path.join(f"{abs_folder_path}", "activities", form_name) + filename = f"{form_name}_schema" + file_path = os.path.join(path, filename) + try: + os.makedirs(path, exist_ok=True) + with open(file_path, "w") as file: + json.dump(json_ld, file, indent=4) + print(f"{form_name} Instrument schema created") + except OSError as e: + print(f"Error creating directory {path}: {e}") + except IOError as e: + print(f"Error writing to file {file_path}: {e}") + + +def process_activities(activity_name, protocol_visibility_obj, protocol_order): + # Set default visibility condition + protocol_visibility_obj[activity_name] = True + + protocol_order.append(activity_name) + + +def create_protocol_schema( + abs_folder_path, + schema_context_url, + protocol_name, + protocol_display_name, + protocol_description, + protocol_order, + protocol_visibility_obj, +): + # Construct the protocol schema + protocol_schema = { + "@context": schema_context_url, + "@type": "reproschema:Protocol", + "@id": f"{protocol_name}_schema", + "skos:prefLabel": protocol_display_name, + "skos:altLabel": f"{protocol_name}_schema", + "schema:description": protocol_description, + "schema:schemaVersion": "1.0.0-rc4", + "schema:version": "0.0.1", + "ui": { + "addProperties": [], + "order": protocol_order, + "shuffle": False, + }, + } + + # Populate addProperties list + for activity in protocol_order: + add_property = { + "isAbout": f"../activities/{activity}/{activity}_schema", + "variableName": f"{activity}_schema", + # Assuming activity name as prefLabel, update as needed + "prefLabel": activity.replace("_", " ").title(), + } + protocol_schema["ui"]["addProperties"].append(add_property) + + # Add visibility if needed + if protocol_visibility_obj: + protocol_schema["ui"]["visibility"] = protocol_visibility_obj + + protocol_dir = f"{abs_folder_path}/{protocol_name}" + schema_file = f"{protocol_name}_schema" + file_path = os.path.join(protocol_dir, schema_file) + + try: + os.makedirs(protocol_dir, exist_ok=True) + with open(file_path, "w") as file: + json.dump(protocol_schema, file, indent=4) + print("Protocol schema created") + except OSError as e: + print(f"Error creating directory {protocol_dir}: {e}") + except IOError as e: + print(f"Error writing to file {file_path}: {e}") + + +def parse_language_iso_codes(input_string): + soup = BeautifulSoup(input_string, "lxml") + return [element.get("lang") for element in soup.find_all(True, {"lang": True})] + + +def process_csv( + csv_file, + abs_folder_path, + schema_context_url, + schema_map, + input_type_map, + ui_list, + response_list, + additional_notes_list, + protocol_name, +): + datas = {} + order = {} + languages = [] + + with open(csv_file, mode="r", encoding="utf-8") as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + form_name = row["Form Name"] + if form_name not in datas: + datas[form_name] = [] + order[form_name] = [] + os.makedirs( + f"{abs_folder_path}/activities/{form_name}/items", exist_ok=True + ) + + datas[form_name].append(row) + + if not languages: + languages = parse_language_iso_codes(row["Field Label"]) + + for field in datas[form_name]: + field_name = field["Variable / Field Name"] + order[form_name].append(f"items/{field_name}") + process_row( + abs_folder_path, + schema_context_url, + form_name, + field, + schema_map, + input_type_map, + ui_list, + response_list, + additional_notes_list, + ) + + os.makedirs(f"{abs_folder_path}/{protocol_name}", exist_ok=True) + return datas, order, languages + + +def redcap2reproschema(csv_file, yaml_file, schema_context_url=None): + """ + Convert a REDCap data dictionary to Reproschema format. + + :param csv_file: Path to the REDCap CSV file. + :param yaml_path: Path to the YAML configuration file. + :param schema_context_url: URL of the schema context. Optional. + """ + + # Read the YAML configuration + with open(yaml_file, "r") as f: + protocol = yaml.safe_load(f) + + protocol_name = protocol.get("protocol_name") + protocol_display_name = protocol.get("protocol_display_name") + protocol_description = protocol.get("protocol_description") + + if not protocol_name: + raise ValueError("Protocol name not specified in the YAML file.") + + protocol_name = protocol_name.replace(" ", "_") # Replacing spaces with underscores + + # Check if the directory already exists + if not os.path.exists(protocol_name): + os.mkdir(protocol_name) # Create the directory if it doesn't exist + + # Get absolute path of the local repository + abs_folder_path = os.path.abspath(protocol_name) + + if schema_context_url is None: + schema_context_url = "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic" + + # Initialize variables + schema_map = { + "Variable / Field Name": "@id", # column A + "Item Display Name": "prefLabel", + "Field Annotation": "description", # column R + "Section Header": "preamble", # column C (need double-check) + "Field Label": "question", # column E + "Field Type": "inputType", # column D + "Allow": "allow", + "Required Field?": "requiredValue", # column M + "Text Validation Min": "minValue", # column I + "Text Validation Max": "maxValue", # column J + "Choices, Calculations, OR Slider Labels": "choices", # column F + "Branching Logic (Show field only if...)": "visibility", # column L + "Custom Alignment": "customAlignment", # column N + "Identifier?": "identifiable", # column K + "multipleChoice": "multipleChoice", + "responseType": "@type", + } + + input_type_map = { + "calc": "number", + "checkbox": "radio", + "descriptive": "static", + "dropdown": "select", + "notes": "text", + } + + ui_list = ["inputType", "shuffle", "allow", "customAlignment"] + response_list = [ + "valueType", + "minValue", + "maxValue", + "requiredValue", + "multipleChoice", + ] + additional_notes_list = ["Field Note", "Question Number (surveys only)"] + + # Process the CSV file + datas, order, _ = process_csv( + csv_file, + abs_folder_path, + schema_context_url, + schema_map, + input_type_map, + ui_list, + response_list, + additional_notes_list, + protocol_name, + ) + # Initialize other variables for protocol context and schema + protocol_visibility_obj = {} + protocol_order = [] + + # Create form schemas and process activities + for form_name, rows in datas.items(): + bl_list = [] + scores_list = [] + matrix_list = [] + + for field in rows: + visibility_obj = process_visibility(field) + bl_list.append(visibility_obj) + + if field.get("Matrix Group Name") or field.get("Matrix Ranking?"): + matrix_list.append( + { + "variableName": field["Variable / Field Name"], + "matrixGroupName": field["Matrix Group Name"], + "matrixRanking": field["Matrix Ranking?"], + } + ) + + activity_display_name = rows[0]["Form Name"] + activity_description = rows[0].get("Form Note", "Default description") + + create_form_schema( + abs_folder_path, + schema_context_url, + form_name, + activity_display_name, + activity_description, + order, + bl_list, + matrix_list, + scores_list, + ) + + process_activities(form_name, protocol_visibility_obj, protocol_order) + + # Create protocol schema + create_protocol_schema( + abs_folder_path, + schema_context_url, + protocol_name, + protocol_display_name, + protocol_description, + protocol_order, + protocol_visibility_obj, + ) + + +def main(): + parser = argparse.ArgumentParser( + description="Convert REDCap data dictionary to Reproschema format." + ) + parser.add_argument("csv_file", help="Path to the REDCap data dictionary CSV file.") + parser.add_argument("yaml_file", help="Path to the Reproschema protocol YAML file.") + args = parser.parse_args() + + # Call the main conversion function + redcap2reproschema(args.csv_file, args.yaml_file) + + +if __name__ == "__main__": + main() diff --git a/reproschema/tests/test_data/redcap2rs.yaml b/reproschema/tests/test_data/redcap2rs.yaml new file mode 100644 index 0000000..3330f3b --- /dev/null +++ b/reproschema/tests/test_data/redcap2rs.yaml @@ -0,0 +1,21 @@ +# Reproschema Protocol Configuration + +# Protocol Name: +# Use underscores for spaces and avoid special characters. +# This is the unique identifier for your protocol. +protocol_name: "test_redcap2rs" # Example: "My_Protocol" + +# Protocol Display Name: +# This name will be displayed in the application. +protocol_display_name: "redcap protocols" + +# GitHub Repository Information: +# Create a GitHub repository named 'reproschema' to store your reproschema protocols. +# Replace 'your_github_username' with your actual GitHub username. +user_name: "yibeichan" +repo_name: "redcap2reproschema" # Recommended name; can be different if preferred. +repo_url: "https://github.com/{{user_name}}/{{repo_name}}" + +# Protocol Description: +# Provide a brief description of your protocol. +protocol_description: "testing" # Example: "This protocol is for ..." diff --git a/reproschema/tests/test_data/redcap_dict.csv b/reproschema/tests/test_data/redcap_dict.csv new file mode 100644 index 0000000..6f48394 --- /dev/null +++ b/reproschema/tests/test_data/redcap_dict.csv @@ -0,0 +1,31 @@ +"Variable / Field Name","Form Name","Section Header","Field Type","Field Label","Choices, Calculations, OR Slider Labels","Field Note","Text Validation Type OR Show Slider Number","Text Validation Min","Text Validation Max",Identifier?,"Branching Logic (Show field only if...)","Required Field?","Custom Alignment","Question Number (surveys only)","Matrix Group Name","Matrix Ranking?","Field Annotation" +record_id,autism_parenting_stress_index_apsi,,text,"Record ID",,,,,,,,,,,,, +apsi_date,autism_parenting_stress_index_apsi,"Autism Parenting Stress Index for the Qigong Sensory Training Program Instructions: 1. Before beginning Qigong Sensory Training therapy with your child, complete the form on the following page. 2. Enter the date, name of your child, and who is completing the checklist. (It is very important that the same parent/caretaker complete the form each time the form is used.) 3. Choose the response for each item that most accurately describes your child. 4. Add all of the numbers chosen. 5. Enter total into the space provided. After using Qigong Sensory Training therapy on your child once a day for a five months, have the same parent complete the form again. Total numbers circled. Compare this number to the number at the beginning. If Qigong Sensory Training therapy is being implemented successfully, the total number should decrease over time.",text,Date:,,,date_ymd,,,,,,,,,, +apsi_name_of_child,autism_parenting_stress_index_apsi,,text,"Name of child:",,,,,,,,,,,,, +apsi_person_completing,autism_parenting_stress_index_apsi,,text,"Person completing checklist:",,,,,,,,,,,,, +apsi_social_dev,autism_parenting_stress_index_apsi,"Stress Ratings Please rate the following aspects of your child's health according to how much stress it causes you and/or your family by clicking on the button that best describes your situation.",radio,"Your child's social development ","0, 0 - Not stressful | 1, 1 - Sometimes creates stress | 2, 2 - Often creates stress | 3, 3 - Very stressful on a daily basis | 5, 5 - So stressful sometimes we feel we can't cope",,,,,,,,,,,, +apsi_communicate,autism_parenting_stress_index_apsi,,radio,"Your child's ability to communicate ","0, 0 - Not stressful | 1, 1 - Sometimes creates stress | 2, 2 - Often creates stress | 3, 3 - Very stressful on a daily basis | 5, 5 - So stressful sometimes we feel we can't cope",,,,,,,,,,,, +apsi_tantrums,autism_parenting_stress_index_apsi,,radio,"Tantrums/meltdowns ","0, 0 - Not stressful | 1, 1 - Sometimes creates stress | 2, 2 - Often creates stress | 3, 3 - Very stressful on a daily basis | 5, 5 - So stressful sometimes we feel we can't cope",,,,,,,,,,,, +apsi_agressive,autism_parenting_stress_index_apsi,,radio,"Aggressive behavior (siblings, peers) ","0, 0 - Not stressful | 1, 1 - Sometimes creates stress | 2, 2 - Often creates stress | 3, 3 - Very stressful on a daily basis | 5, 5 - So stressful sometimes we feel we can't cope",,,,,,,,,,,, +apsi_self_injure,autism_parenting_stress_index_apsi,,radio,"Self-injurious behavior ","0, 0 - Not stressful | 1, 1 - Sometimes creates stress | 2, 2 - Often creates stress | 3, 3 - Very stressful on a daily basis | 5, 5 - So stressful sometimes we feel we can't cope",,,,,,,,,,,, +apsi_transitions,autism_parenting_stress_index_apsi,,radio,"Difficulty making transitions from one activity to another ","0, 0 - Not stressful | 1, 1 - Sometimes creates stress | 2, 2 - Often creates stress | 3, 3 - Very stressful on a daily basis | 5, 5 - So stressful sometimes we feel we can't cope",,,,,,,,,,,, +apsi_sleep,autism_parenting_stress_index_apsi,,radio,"Sleep problems ","0, 0 - Not stressful | 1, 1 - Sometimes creates stress | 2, 2 - Often creates stress | 3, 3 - Very stressful on a daily basis | 5, 5 - So stressful sometimes we feel we can't cope",,,,,,,,,,,, +apsi_diet,autism_parenting_stress_index_apsi,,radio,"Your child's diet ","0, 0 - Not stressful | 1, 1 - Sometimes creates stress | 2, 2 - Often creates stress | 3, 3 - Very stressful on a daily basis | 5, 5 - So stressful sometimes we feel we can't cope",,,,,,,,,,,, +apsi_bowel,autism_parenting_stress_index_apsi,,radio,"Bowel problems (diarrhea, constipation) ","0, 0 - Not stressful | 1, 1 - Sometimes creates stress | 2, 2 - Often creates stress | 3, 3 - Very stressful on a daily basis | 5, 5 - So stressful sometimes we feel we can't cope",,,,,,,,,,,, +apsi_potty,autism_parenting_stress_index_apsi,,radio,"Potty training ","0, 0 - Not stressful | 1, 1 - Sometimes creates stress | 2, 2 - Often creates stress | 3, 3 - Very stressful on a daily basis | 5, 5 - So stressful sometimes we feel we can't cope",,,,,,,,,,,, +apsi_not_close,autism_parenting_stress_index_apsi,,radio,"Not feeling close to your child ","0, 0 - Not stressful | 1, 1 - Sometimes creates stress | 2, 2 - Often creates stress | 3, 3 - Very stressful on a daily basis | 5, 5 - So stressful sometimes we feel we can't cope",,,,,,,,,,,, +apsi_accepted,autism_parenting_stress_index_apsi,,radio,"Concern for the future of your child being accepted by others ","0, 0 - Not stressful | 1, 1 - Sometimes creates stress | 2, 2 - Often creates stress | 3, 3 - Very stressful on a daily basis | 5, 5 - So stressful sometimes we feel we can't cope",,,,,,,,,,,, +apsi_independently,autism_parenting_stress_index_apsi,,radio,"Concern for the future of your child living independently ","0, 0 - Not stressful | 1, 1 - Sometimes creates stress | 2, 2 - Often creates stress | 3, 3 - Very stressful on a daily basis | 5, 5 - So stressful sometimes we feel we can't cope",,,,,,,,,,,, +apsi_total,autism_parenting_stress_index_apsi,,text,Total,,,integer,,,,,,,,,, +cams_r_1,cognitive_and_affective_mindfulness_scalerevised_c,"Instructions: People have a variety of ways of relating to their thoughts and feelings. For each of the items below, rate how much each of these ways applies to you.",radio,"1. It is easy for me to concentrate on what I am doing.","1, 1 - Rarely/Not at all | 2, 2 - Sometimes | 3, 3 - Often | 4, 4 - Almost Always",,,,,,,,,,,, +cams_r_2,cognitive_and_affective_mindfulness_scalerevised_c,,radio,"2. I am preoccupied by the future.","1, 1 - Rarely/Not at all | 2, 2 - Sometimes | 3, 3 - Often | 4, 4 - Almost Always",,,,,,,,,,,, +cams_r_3,cognitive_and_affective_mindfulness_scalerevised_c,,radio,"3. I can tolerate emotional pain.","1, 1 - Rarely/Not at all | 2, 2 - Sometimes | 3, 3 - Often | 4, 4 - Almost Always",,,,,,,,,,,, +cams_r_4,cognitive_and_affective_mindfulness_scalerevised_c,,radio,"4. I can accept things I cannot change.","1, 1 - Rarely/Not at all | 2, 2 - Sometimes | 3, 3 - Often | 4, 4 - Almost Always",,,,,,,,,,,, +cams_r_5,cognitive_and_affective_mindfulness_scalerevised_c,,radio,"5. I can usually describe how I feel at the moment in considerable detail.","1, 1 - Rarely/Not at all | 2, 2 - Sometimes | 3, 3 - Often | 4, 4 - Almost Always",,,,,,,,,,,, +cams_r_6,cognitive_and_affective_mindfulness_scalerevised_c,,radio,"6. I am easily distracted.","1, 1 - Rarely/Not at all | 2, 2 - Sometimes | 3, 3 - Often | 4, 4 - Almost Always",,,,,,,,,,,, +cams_r_7,cognitive_and_affective_mindfulness_scalerevised_c,,radio,"7. I am preoccupied by the past.","1, 1 - Rarely/Not at all | 2, 2 - Sometimes | 3, 3 - Often | 4, 4 - Almost Always",,,,,,,,,,,, +cams_r_8,cognitive_and_affective_mindfulness_scalerevised_c,,radio,"8. It's easy for me to keep track of my thoughts and feelings.","1, 1 - Rarely/Not at all | 2, 2 - Sometimes | 3, 3 - Often | 4, 4 - Almost Always",,,,,,,,,,,, +cams_r_9,cognitive_and_affective_mindfulness_scalerevised_c,,radio,"9. I try to notice my thoughts without judging them.","1, 1 - Rarely/Not at all | 2, 2 - Sometimes | 3, 3 - Often | 4, 4 - Almost Always",,,,,,,,,,,, +cams_r_10,cognitive_and_affective_mindfulness_scalerevised_c,,radio,"10. I am able to accept the thoughts and feelings I have.","1, 1 - Rarely/Not at all | 2, 2 - Sometimes | 3, 3 - Often | 4, 4 - Almost Always",,,,,,,,,,,, +cams_r_11,cognitive_and_affective_mindfulness_scalerevised_c,,radio,"11. I am able to focus on the present moment.","1, 1 - Rarely/Not at all | 2, 2 - Sometimes | 3, 3 - Often | 4, 4 - Almost Always",,,,,,,,,,,, +cams_r_12,cognitive_and_affective_mindfulness_scalerevised_c,,radio,"12. I am able to pay close attention to one thing for a long period of time.","1, 1 - Rarely/Not at all | 2, 2 - Sometimes | 3, 3 - Often | 4, 4 - Almost Always",,,,,,,,,,,, diff --git a/reproschema/tests/test_redcap2reproschema.py b/reproschema/tests/test_redcap2reproschema.py new file mode 100644 index 0000000..de2f630 --- /dev/null +++ b/reproschema/tests/test_redcap2reproschema.py @@ -0,0 +1,27 @@ +import os +import shutil +import pytest +from click.testing import CliRunner +from ..cli import main # Import the Click group + +# Assuming your test files are located in a 'tests' directory +CSV_FILE_NAME = "redcap_dict.csv" +YAML_FILE_NAME = "redcap2rs.yaml" +CSV_TEST_FILE = os.path.join(os.path.dirname(__file__), "test_data", CSV_FILE_NAME) +YAML_TEST_FILE = os.path.join(os.path.dirname(__file__), "test_data", YAML_FILE_NAME) + + +def test_redcap2reproschema_success(): + runner = CliRunner() + + with runner.isolated_filesystem(): + # Copy the test files to the isolated filesystem + shutil.copy(CSV_TEST_FILE, CSV_FILE_NAME) + shutil.copy(YAML_TEST_FILE, YAML_FILE_NAME) + + # Run the command within the isolated filesystem + result = runner.invoke( + main, ["redcap2reproschema", CSV_FILE_NAME, YAML_FILE_NAME] + ) + print(result.output) + assert result.exit_code == 0 diff --git a/setup.cfg b/setup.cfg index f01cdfc..8311757 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,6 +32,8 @@ install_requires = requests requests_cache pyyaml + beautifulsoup4 + lxml test_requires = pytest >= 4.4.0 diff --git a/templates/redcap2rs.yaml b/templates/redcap2rs.yaml new file mode 100644 index 0000000..1e1dbc3 --- /dev/null +++ b/templates/redcap2rs.yaml @@ -0,0 +1,14 @@ +# Reproschema Protocol Configuration + +# Protocol Name: +# Use underscores for spaces and avoid special characters. +# This is the unique identifier for your protocol. +protocol_name: "your_protocol_name" # Example: "My_Protocol" + +# Protocol Display Name: +# This name will be displayed in the application. +protocol_display_name: "Your protocol display name" + +# Protocol Description: +# Provide a brief description of your protocol. +protocol_description: "Description for your protocol" # Example: "This protocol is for ..."