Skip to content

Commit e495d80

Browse files
committed
Merge branch 'release/1.14.0'
2 parents 4818f39 + b17e9bc commit e495d80

7 files changed

+83
-47
lines changed

CHANGELOG

+14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
Changelog
22
=========
3+
## [1.14.0] - 2024-08-26
4+
### Summary
5+
This release adds support for multiple REDCap projects, in case the Optional Modules such as LBD or FTLD are broken out into their own REDCap projects. To pull from multiple REDCap projects into the same `redcap_input.csv` file, create a list of API tokens in your config file. This update also changes NACCulator's event detection to require the presence of both the Z1X and the A1 forms for UDS visits.
6+
7+
### Added
8+
* Add additional unit test (Michael Bentz)
9+
* Add pandas to setup.py (Michael Bentz)
10+
* Add script to combine REDCap project data (Michael Bentz)
11+
12+
### Updated
13+
* Update README with new requirements for ivp and fvp processing (Samantha Emerson)
14+
* Add A1 to required forms for IVP and FVP processing (Samantha Emerson)
15+
* Bump report_handler dependency to 1.3.0 (Michael Bentz)
16+
317
## [1.13.1] - 2024-04-15
418
### Summary
519
This release removes the `.vscode` sub-module from the NACCulator repo, since it points to a private repository and makes NACCulator difficult to install for those who do not have permissions for the .vscode repo.

README.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ marked "Unverified" or "Complete" for NACCulator to recognize the record, and
4545
each optional form must be marked as submitted within the Z1X for NACCulator to
4646
find those forms.
4747

48+
_Note: For UDS visits (the -ivp and -fvp flags), NACCulator also expects the
49+
A1 subject demographics form to be either Unverified or Complete._
50+
4851
_Note: output is written to `STDOUT`; errors are written to `STDERR`; input is
4952
expected to be from `STDIN` (the command line) unless a file is specified using
5053
the `-file` flag._
@@ -351,7 +354,7 @@ This is not exhaustive, but here is an explanation of some important files.
351354

352355
* `nacc/run_filters.py` and `tools/preprocess/run_filters.sh`:
353356
pulls data from REDCap based on the settings found in `nacculator_cfg.ini`
354-
(for .py) and `filters_config.cfg` (for .sh).
357+
(for .py) and `filters_config.cfg` (for .sh). Supports exporting data from multiple REDCap projects by adding a comma-delimited list of tokens without spaces e.g., `token=token1,token2` to `token` in the `nacculator_cfg.ini` config file.
355358

356359

357360
### Testing

nacc/redcap2nacc.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,12 @@ def check_redcap_event(
237237
form_match_z1 = ''
238238
record['ivp_z1_complete'] = ''
239239
form_match_z1x = record['ivp_z1x_complete']
240-
if form_match_z1 in ['0', ''] and form_match_z1x in ['0', '']:
240+
try:
241+
form_match_a1 = record['ivp_a1_complete']
242+
except KeyError:
243+
form_match_a1 = record['ivp_a1_subject_demographics_complete']
244+
if (form_match_z1 in ['0', ''] and form_match_z1x in ['0', '']) or \
245+
form_match_a1 in ['0', '']:
241246
return False
242247
elif options.fvp:
243248
event_name = 'follow'
@@ -247,7 +252,12 @@ def check_redcap_event(
247252
form_match_z1 = ''
248253
record['fvp_z1_complete'] = ''
249254
form_match_z1x = record['fvp_z1x_complete']
250-
if form_match_z1 in ['0', ''] and form_match_z1x in ['0', '']:
255+
try:
256+
form_match_a1 = record['fvp_a1_complete']
257+
except KeyError:
258+
form_match_a1 = record['fvp_a1_subject_demographics_complete']
259+
if (form_match_z1 in ['0', ''] and form_match_z1x in ['0', '']) or \
260+
form_match_a1 in ['0', '']:
251261
return False
252262
# TODO: add -csf option if/when it is added to the full ADRC project.
253263
elif options.cv:

nacc/run_filters.py

+37-38
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from redcap import Project
77
from nacc.uds3.filters import *
88
import logging
9+
import pandas as pd
910

1011

1112
# Creating a folder which contains Intermediate files
@@ -112,7 +113,7 @@ def get_data_from_redcap_pycap(folder_name, config):
112113
"""
113114
# Enter the path for filters_config
114115
try:
115-
token = config.get('pycap', 'token')
116+
tokens = config.get('pycap', 'token').split(',')
116117
redcap_url = config.get('pycap', 'redcap_server')
117118
except Exception as e:
118119
print("Please check the config file and validate all the proper fields exist", file=sys.stderr)
@@ -130,7 +131,32 @@ def get_data_from_redcap_pycap(folder_name, config):
130131
print(e)
131132
raise e
132133

133-
redcap_project = Project(redcap_url, token)
134+
try:
135+
df_all_project_data = pd.DataFrame()
136+
for token in tokens:
137+
df_project_data = _get_project_data(redcap_url, token)
138+
df_all_project_data = pd.concat([df_all_project_data, df_project_data], ignore_index=True)
139+
140+
# Ignore index to remove the record number column
141+
df_all_project_data.to_csv(os.path.join(folder_name, "redcap_input.csv"), index=False)
142+
except Exception as e:
143+
print("Error in exporting project data")
144+
logging.error('Error in processing project data',
145+
extra={
146+
"report_handler": {
147+
"data": {"ptid": None, "error": f'Error in exporting project data: {e}'},
148+
"sheet": 'ERROR'
149+
}
150+
}
151+
)
152+
print(e)
153+
154+
return
155+
156+
157+
def _get_project_data(url: str, token: str) -> pd.DataFrame:
158+
df_project_data = pd.DataFrame()
159+
redcap_project = Project(url, token)
134160

135161
# Get list of all fieldnames in project to create a csv header
136162
assert hasattr(redcap_project, 'field_names')
@@ -146,8 +172,13 @@ def get_data_from_redcap_pycap(folder_name, config):
146172

147173
# Get list of all records present in project to iterate over
148174
list_of_records = []
175+
176+
# If there are multiple records with the same ptid only keep the first occurence
177+
# export all records only grabbing the ptid field
149178
all_records = redcap_project.export_records(fields=['ptid'])
179+
# iterate through all records
150180
for record in all_records:
181+
# if the ptid is not already in the list then append it
151182
if record['ptid'] not in list_of_records:
152183
list_of_records.append(record['ptid'])
153184

@@ -157,43 +188,11 @@ def get_data_from_redcap_pycap(folder_name, config):
157188
for i in range(0, len(list_of_records), n):
158189
chunked_records.append(list_of_records[i:i + n])
159190

160-
try:
161-
162-
try:
163-
with open(os.path.join(folder_name, "redcap_input.csv"), "w") as redcap_export:
164-
writer = csv.DictWriter(redcap_export, fieldnames=header_full)
165-
writer.writeheader()
166-
# header_mapping = next(reader)
167-
for current_record_chunk in chunked_records:
168-
data = redcap_project.export_records(
169-
records=current_record_chunk)
170-
for row in data:
171-
writer.writerow(row)
172-
except Exception as e:
173-
print("Error in Writing")
174-
logging.error('Error in writing',
175-
extra={
176-
"report_handler": {
177-
"data": {"ptid": None, "error": f'Error in writing: {e}'},
178-
"sheet": 'ERROR'
179-
}
180-
}
181-
)
182-
print(e)
191+
for current_record_chunk in chunked_records:
192+
chunked_data = redcap_project.export_records(records=current_record_chunk)
193+
df_project_data = pd.concat([df_project_data, pd.DataFrame(chunked_data)], ignore_index=True)
183194

184-
except Exception as e:
185-
print("Error in CSV file")
186-
logging.error('Error in CSV file',
187-
extra={
188-
"report_handler": {
189-
"data": {"ptid": None,
190-
"error": f'Error in CSV file: {e}'},
191-
"sheet": 'ERROR'
192-
}
193-
}
194-
)
195-
196-
return
195+
return df_project_data
197196

198197

199198
def main():

nacculator_cfg.ini.example

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
[DEFAULT]
22

33
[pycap]
4+
redcap_server: http://<redcap_server>/api/
5+
# token now supports multiple tokens as a comma-delimeted list without spaces e.g., token1,token2,token3,token4
46
token: Your REDCAP Token
5-
redcap_server: Your Redcap Server
67

78
# [filters] - Each section is named after the corresponding function name
89
# in filters.py

setup.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from setuptools import setup, find_packages
88

9-
VERSION = "1.13.1"
9+
VERSION = "1.14.0"
1010

1111
setup(
1212
name="nacculator",
@@ -33,7 +33,8 @@
3333

3434
install_requires=[
3535
"PyCap>=2.1.0",
36-
"report_handler @ git+https://git@github.com:/ctsit/report_handler.git"
36+
"pandas>=2.2.0",
37+
"report_handler @ git+https://git@github.com:/ctsit/report_handler.git@1.3.0"
3738
],
3839

3940
python_requires=">=3.6.0",

tests/test_check_redcap_event.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,26 @@ def test_for_ivp(self):
3535
'''
3636
self.options.ivp = True
3737
record = {'redcap_event_name': 'initial_visit_year_arm_1',
38-
'ivp_z1_complete': '', 'ivp_z1x_complete': '2'}
38+
'ivp_z1_complete': '', 'ivp_z1x_complete': '2',
39+
'ivp_a1_complete': '2'} # condition met to return True
3940
result = check_redcap_event(self.options, record)
4041
self.assertTrue(result)
4142

43+
record = {'redcap_event_name': 'initial_visit_year_arm_1',
44+
'ivp_z1_complete': '', 'ivp_z1x_complete': '2',
45+
'ivp_a1_complete': ''} # condition met to return False
46+
result2 = check_redcap_event(self.options, record)
47+
self.assertFalse(result2)
48+
4249
def test_for_not_ivp(self):
4350
'''
4451
Checks that the initial_visit is not returned when the -ivp flag is not
4552
set.
4653
'''
4754
self.options.fvp = True
4855
record = {'redcap_event_name': 'initial_visit_year_arm_1',
49-
'fvp_z1_complete': '', 'fvp_z1x_complete': ''}
56+
'fvp_z1_complete': '', 'fvp_z1x_complete': '',
57+
'fvp_a1_complete': '2'}
5058
result = check_redcap_event(self.options, record)
5159
self.assertFalse(result)
5260

@@ -68,7 +76,7 @@ def test_for_not_multiple_flags(self):
6876
self.options.ivp = True
6977
record = {'redcap_event_name': 'initial_visit_year_arm_1',
7078
'ivp_z1_complete': '', 'ivp_z1x_complete': '',
71-
'lbd_ivp_b1l_complete': '2'}
79+
'lbd_ivp_b1l_complete': '2', 'ivp_a1_complete': '2'}
7280
incorrect = check_redcap_event(self.options, record)
7381

7482
self.options.lbd = True

0 commit comments

Comments
 (0)