-
Notifications
You must be signed in to change notification settings - Fork 0
feat: ✨ convert REDCap dictionary to resource properties #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 3 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
92fb93a
feat: :sparkles: convert redcap data dict to resource properties
martonvago c3d3ac8
fix: :bug: fix description whitespace
martonvago 096ce2c
refactor: :recycle: use optional
martonvago 9c9f995
fix: :bug: access FieldType properly after Sprout update
martonvago 4091397
feat: :sparkles: use text_validation_type_or_show_slider_number to pi…
martonvago b254c2f
refactor: :recycle: review markups
martonvago f930e85
feat: :sparkles: extract min and max constraints
martonvago 2f9d80a
fix: :bug: rename visit to event everywhere
martonvago 1635296
feat: :sparkles: add generated resource properties in package properties
martonvago 1c4bb24
fix: :bug: add back Danish cpr mask
martonvago a5a6e40
feat: :sparkles: add center field
martonvago 1d1542f
feat: :sparkles: generate datapackage.json from CPH metadata
b97f2e5
Merge branch 'main' of https://github.com/onlimit-study/feasibility-d…
lwjohnst86 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| import json | ||
| import re | ||
| from itertools import chain, groupby | ||
| from operator import itemgetter | ||
| from pathlib import Path | ||
| from typing import Callable, Iterable, Optional, TypeVar, cast | ||
|
|
||
| import seedcase_sprout as sp | ||
|
|
||
| In = TypeVar("In") | ||
| Out = TypeVar("Out") | ||
|
|
||
|
|
||
| def _map(x: Iterable[In], fn: Callable[[In], Out]) -> list[Out]: | ||
| return list(map(fn, x)) | ||
|
|
||
|
|
||
| def _filter(x: Iterable[In], fn: Callable[[In], bool]) -> list[In]: | ||
| return list(filter(fn, x)) | ||
|
|
||
|
|
||
| def _flat_map(items: Iterable[In], fn: Callable[[In], Iterable[Out]]) -> list[Out]: | ||
| """Maps and flattens the items by one level.""" | ||
| return list(chain.from_iterable(map(fn, items))) | ||
|
|
||
|
|
||
| def load_data_dict_from_file() -> list[dict[str, str]]: | ||
| """Loads REDCap data dictionary from `scripts/data_dictionary.json`.""" | ||
| with open(Path("scripts") / "data_dictionary.json") as f: | ||
| return json.load(f) | ||
|
|
||
|
|
||
| def redcap_data_dict_to_resource_properties( | ||
|
lwjohnst86 marked this conversation as resolved.
Outdated
|
||
| redcap_fields: list[dict[str, str]], | ||
| ) -> list[sp.ResourceProperties]: | ||
| """Converts REDCap data dictionary to Data Package resources.""" | ||
| sorted_by_form = sorted(redcap_fields, key=lambda field: field["form_name"]) | ||
| grouped_by_form = groupby(sorted_by_form, key=lambda field: field["form_name"]) | ||
| return _map( | ||
| grouped_by_form, | ||
| lambda group: _redcap_form_to_resource(group[0], list(group[1])), | ||
| ) | ||
|
|
||
|
|
||
| def _redcap_form_to_resource( | ||
|
lwjohnst86 marked this conversation as resolved.
Outdated
|
||
| form_name: str, fields: list[dict[str, str]] | ||
| ) -> sp.ResourceProperties: | ||
| visit_field = sp.FieldProperties( | ||
| name="visit", | ||
|
lwjohnst86 marked this conversation as resolved.
Outdated
|
||
| title="The unique name of the visit.", | ||
| type="string", | ||
| description=( | ||
| "The unique name identifying the visit. A visit " | ||
| "corresponds to a REDCap event when the the form was filled in." | ||
| ), | ||
| ) | ||
|
|
||
| # Discard fields displayed for information only | ||
| form_redcap_fields = _filter( | ||
| fields, lambda field: field["field_type"] not in ["descriptive", "checkbox"] | ||
| ) | ||
| form_fields = _map( | ||
| form_redcap_fields, | ||
| lambda field: sp.FieldProperties( | ||
| name=field["field_name"], | ||
| title=field["field_name"], | ||
| type=_get_type(field), | ||
| description=_get_description(field), | ||
| categories=_get_categories(field), | ||
| constraints=sp.ConstraintsProperties( | ||
| required=_get_required(field), | ||
| enum=_get_categories(field), | ||
| ), | ||
| ), | ||
| ) | ||
|
|
||
| checkbox_redcap_fields = _filter( | ||
| fields, lambda field: field["field_type"] == "checkbox" | ||
| ) | ||
| checkbox_fields = _flat_map(checkbox_redcap_fields, _expand_checkbox_field) | ||
|
|
||
| return sp.ResourceProperties( | ||
| name=form_name, | ||
| # TODO: fill in title and description | ||
| title=form_name, | ||
| description=form_name, | ||
| schema=sp.TableSchemaProperties( | ||
| primary_key=["visit"], | ||
| fields=[visit_field] + form_fields + checkbox_fields, | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| def _get_error_message(field: dict[str, str], key: str) -> str: | ||
| return ( | ||
| f"Unexpected value {field[key]!r} for `{key}` in field {field['field_name']!r} " | ||
| f"in form {field['form_name']!r}." | ||
| ) | ||
|
|
||
|
|
||
| def _get_choices(field: dict[str, str]) -> list[tuple[str, str]]: | ||
| """Parses the choices into the choice number and choice value. | ||
|
|
||
| E.g.: | ||
| Input: "1, first choice|2, second choice|3, third choice" | ||
| Output: [('1', 'first choice'), ('2', 'second choice'), ('3', 'third choice')] | ||
| """ | ||
| choices = field["select_choices_or_calculations"].split("|") | ||
| matches = _map(choices, lambda choice: re.match(r"^(\d+), *(.*)", choice.strip())) | ||
| if not all(matches): | ||
| raise ValueError(_get_error_message(field, "select_choices_or_calculations")) | ||
| return _map( | ||
| cast(list[re.Match], matches), | ||
| lambda match: (match.group(1), match.group(2)), | ||
| ) | ||
|
|
||
|
|
||
| def _expand_checkbox_field(checkbox_field: dict[str, str]) -> list[sp.FieldProperties]: | ||
| return _map( | ||
| _get_choices(checkbox_field), | ||
| lambda choice: sp.FieldProperties( | ||
| name=f"{checkbox_field['field_name']}___{choice[0]}", | ||
| title=choice[1], | ||
| type="boolean", | ||
| description=_get_description(checkbox_field), | ||
| constraints=sp.ConstraintsProperties( | ||
| required=_get_required(checkbox_field), | ||
| ), | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| def _get_required(redcap_field: dict[str, str]) -> bool: | ||
| match redcap_field["required_field"]: | ||
| case "y": | ||
| return True | ||
| case "": | ||
| return False | ||
| case _: | ||
| raise NotImplementedError( | ||
| _get_error_message(redcap_field, "required_field") | ||
| ) | ||
|
|
||
|
|
||
| def _get_description(redcap_field: dict[str, str]) -> str: | ||
| description = redcap_field["field_annotation"] | ||
| if redcap_field["field_type"] == "calc": | ||
| description += ( | ||
| " Derived using the formula: " | ||
| + redcap_field["select_choices_or_calculations"] | ||
| ) | ||
|
|
||
| if redcap_field["field_type"] == "slider": | ||
| description += ( | ||
| f" Question: {redcap_field['field_label']}. Slider scale labels: " | ||
| # Given as: left label | middle label | right label | ||
| + redcap_field["select_choices_or_calculations"] | ||
| ) | ||
|
|
||
| return description.strip() | ||
|
|
||
|
|
||
| def _get_categories(redcap_field: dict[str, str]) -> Optional[list[str]]: | ||
| if redcap_field["field_type"] != "radio": | ||
| return None | ||
|
|
||
| return _map(_get_choices(redcap_field), itemgetter(1)) | ||
|
|
||
|
|
||
| def _get_type(redcap_field: dict[str, str]) -> sp.properties.FieldType: | ||
| match redcap_field["field_type"]: | ||
| case "text" | "calc" | "radio" | "notes" | "file": | ||
| return "string" | ||
| case "slider": | ||
| return "number" | ||
| case _: | ||
| raise NotImplementedError(_get_error_message(redcap_field, "field_type")) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.