Esempio n. 1
0
class ProjectTests(unittest.TestCase):
    """docstring for ProjectTests"""

    long_proj_url = 'https://redcap.longproject.edu/api/'
    normal_proj_url = 'https://redcap.normalproject.edu/api/'
    ssl_proj_url = 'https://redcap.sslproject.edu/api/'
    survey_proj_url = 'https://redcap.surveyproject.edu/api/'
    bad_url = 'https://redcap.badproject.edu/api'
    reg_token = 'supersecrettoken'

    def setUp(self):
        self.create_projects()

    def tearDown(self):
        pass

    def add_long_project_response(self):
        def request_callback_long(request):
            parsed = urlparse.urlparse("?{}".format(request.body))
            data = urlparse.parse_qs(parsed.query)
            headers = {"Content-Type": "application/json"}

            request_type = data["content"][0]

            if "returnContent" in data:
                resp = {"count": 1}

            elif (request_type == "metadata"):
                resp = [{
                    'field_name': 'record_id',
                    'field_label': 'Record ID',
                    'form_name': 'Test Form',
                    "arm_num": 1,
                    "name": "test"
                }]
            elif (request_type == "version"):
                resp = b'8.6.0'
                headers = {'content-type': 'text/csv; charset=utf-8'}
                return (201, headers, resp)
            elif (request_type == "event"):
                resp = [{
                    'unique_event_name': "raw"
                }]
            elif (request_type == "arm"):
                resp = [{
                    "arm_num": 1,
                    "name": "test"
                }]
            elif (request_type in ["record", "formEventMapping"]):
                if "csv" in data["format"]:
                    resp = "record_id,test,redcap_event_name\n1,1,raw"
                    headers = {'content-type': 'text/csv; charset=utf-8'}
                    return (201, headers, resp)
                else:
                    resp = [{"field_name":"record_id"}, {"field_name":"test"}]

            return (201, headers, json.dumps(resp))

        responses.add_callback(
            responses.POST,
            self.long_proj_url,
            callback=request_callback_long,
            content_type="application/json",
        )

    def add_normalproject_response(self):
        def request_callback_normal(request):
            parsed = urlparse.urlparse("?{}".format(request.body))
            data = urlparse.parse_qs(parsed.query)
            headers = {"Content-Type": "application/json"}

            if " filename" in data:
                resp = {}
            else:
                request_type = data.get("content", ['unknown'])[0]

                if "returnContent" in data:
                    if "non_existent_key" in data["data"][0]:
                        resp = {"error": "invalid field"}
                    else:
                        resp = {"count": 1}
                elif (request_type == "metadata"):
                    if "csv" in data["format"]:
                        resp = "field_name,field_label,form_name,arm_num,name\n"\
                            "record_id,Record ID,Test Form,1,test\n"
                        headers = {'content-type': 'text/csv; charset=utf-8'}
                        return (201, headers, resp)

                    else:
                        resp = [{
                            'field_name': 'record_id',
                            'field_label': 'Record ID',
                            'form_name': 'Test Form',
                            "arm_num": 1,
                            "name": "test",
                            "field_type": "text",
                        }, {
                            'field_name': 'file',
                            'field_label': 'File',
                            'form_name': 'Test Form',
                            "arm_num": 1,
                            "name": "file",
                            "field_type": "file",
                        }, {
                            'field_name': 'dob',
                            'field_label': 'Date of Birth',
                            'form_name': 'Test Form',
                            "arm_num": 1,
                            "name": "dob",
                            "field_type": "date",
                        }]
                elif (request_type == "version"):
                    resp = {
                        'error': "no version info"
                    }
                elif (request_type == "event"):
                    resp = {
                        'error': "no events"
                    }
                elif (request_type == "arm"):
                    resp = {
                        'error': "no arm"
                    }
                elif (request_type == "record"):
                    if "csv" in data["format"]:
                        resp = "record_id,test,first_name,study_id\n1,1,Peter,1"
                        headers = {'content-type': 'text/csv; charset=utf-8'}
                        return (201, headers, resp)
                    elif "exportDataAccessGroups" in data:
                        resp = [
                            {"field_name":"record_id", "redcap_data_access_group": "group1"},
                            {"field_name":"test", "redcap_data_access_group": "group1"}
                        ]
                    elif "label" in data.get("rawOrLabel"):
                        resp = [{"matcheck1___1": "Foo"}]
                    else:
                        resp = [
                            {"record_id": "1", "test": "test1"},
                            {"record_id": "2", "test": "test"}
                        ]
                elif (request_type == "file"):
                    resp = {}
                    headers["content-type"] = "text/plain;name=data.txt"
                elif (request_type == "user"):
                    resp = [
                        {
                            'firstname': "test",
                            'lastname': "test",
                            'email': "test",
                            'username': "******",
                            'expiration': "test",
                            'data_access_group': "test",
                            'data_export': "test",
                            'forms': "test"
                        }
                    ]

            return (201, headers, json.dumps(resp))

        responses.add_callback(
            responses.POST,
            self.normal_proj_url,
            callback=request_callback_normal,
            content_type="application/json",
        )

    def add_ssl_project(self):
        def request_callback_ssl(request):
            parsed = urlparse.urlparse("?{}".format(request.body))
            data = urlparse.parse_qs(parsed.query)

            request_type = data["content"][0]
            if (request_type == "metadata"):
                resp = [{
                    'field_name': 'record_id',
                    'field_label': 'Record ID',
                    'form_name': 'Test Form',
                    "arm_num": 1,
                    "name": "test"
                }]
            if (request_type == "version"):
                resp = {
                    'error': "no version info"
                }
            if (request_type == "event"):
                resp = {
                    'error': "no events"
                }
            if (request_type == "arm"):
                resp = {
                    'error': "no arm"
                }

            headers = {"Content-Type": "application/json"}
            return (201, headers, json.dumps(resp))

        responses.add_callback(
            responses.POST,
            self.ssl_proj_url,
            callback=request_callback_ssl,
            content_type="application/json",
        )

    def add_survey_project(self):
        def request_callback_survey(request):
            parsed = urlparse.urlparse("?{}".format(request.body))
            data = urlparse.parse_qs(parsed.query)

            request_type = data["content"][0]
            if (request_type == "metadata"):
                resp = [{
                    'field_name': 'record_id',
                    'field_label': 'Record ID',
                    'form_name': 'Test Form',
                    "arm_num": 1,
                    "name": "test"
                }]
            elif (request_type == "version"):
                resp = {
                    'error': "no version info"
                }
            elif (request_type == "event"):
                resp = {
                    'error': "no events"
                }
            elif (request_type == "arm"):
                resp = {
                    'error': "no arm"
                }
            elif (request_type == "record"):
                resp = [
                    {"field_name":"record_id", "redcap_survey_identifier": "test", "demographics_timestamp": "a_real_date"},
                    {"field_name":"test", "redcap_survey_identifier": "test", "demographics_timestamp": "a_real_date"}
                ]

            headers = {"Content-Type": "application/json"}
            return (201, headers, json.dumps(resp))

        responses.add_callback(
            responses.POST,
            self.survey_proj_url,
            callback=request_callback_survey,
            content_type="application/json",
        )

    @responses.activate
    def create_projects(self):
        self.add_long_project_response()
        self.add_normalproject_response()
        self.add_ssl_project()
        self.add_survey_project()

        self.long_proj = Project(self.long_proj_url, self.reg_token)
        self.reg_proj = Project(self.normal_proj_url, self.reg_token)
        self.ssl_proj = Project(self.ssl_proj_url, self.reg_token, verify_ssl=False)
        self.survey_proj = Project(self.survey_proj_url, self.reg_token)


    def test_good_init(self):
        """Ensure basic instantiation """

        self.assertIsInstance(self.long_proj, Project)
        self.assertIsInstance(self.reg_proj, Project)
        self.assertIsInstance(self.ssl_proj, Project)

    def test_normal_attrs(self):
        """Ensure projects are created with all normal attrs"""

        for attr in ('metadata', 'field_names', 'field_labels', 'forms',
            'events', 'arm_names', 'arm_nums', 'def_field'):
            self.assertTrue(hasattr(self.reg_proj, attr))

    def test_long_attrs(self):
        "proj.events/arm_names/arm_nums should not be empty in long projects"

        self.assertIsNotNone(self.long_proj.events)
        self.assertIsNotNone(self.long_proj.arm_names)
        self.assertIsNotNone(self.long_proj.arm_nums)

    def test_is_longitudinal(self):
        "Test the is_longitudinal method"
        self.assertFalse(self.reg_proj.is_longitudinal())
        self.assertTrue(self.long_proj.is_longitudinal())

    def test_regular_attrs(self):
        """proj.events/arm_names/arm_nums should be empty tuples"""
        for attr in 'events', 'arm_names', 'arm_nums':
            attr_obj = getattr(self.reg_proj, attr)
            self.assertIsNotNone(attr_obj)
            self.assertEqual(len(attr_obj), 0)

    @responses.activate
    def test_json_export(self):
        """ Make sure we get a list of dicts"""
        self.add_normalproject_response()
        data = self.reg_proj.export_records()
        self.assertIsInstance(data, list)
        for record in data:
            self.assertIsInstance(record, dict)

    @responses.activate
    def test_long_export(self):
        """After determining a unique event name, make sure we get a
        list of dicts"""
        self.add_long_project_response()
        unique_event = self.long_proj.events[0]['unique_event_name']
        data = self.long_proj.export_records(events=[unique_event])
        self.assertIsInstance(data, list)
        for record in data:
            self.assertIsInstance(record, dict)

    @responses.activate
    def test_import_records(self):
        "Test record import"
        self.add_normalproject_response()
        data = self.reg_proj.export_records()
        response = self.reg_proj.import_records(data)
        self.assertIn('count', response)
        self.assertNotIn('error', response)

    @responses.activate
    def test_import_exception(self):
        "Test record import throws RedcapError for bad import"
        self.add_normalproject_response()
        data = self.reg_proj.export_records()
        data[0]['non_existent_key'] = 'foo'
        with self.assertRaises(RedcapError) as cm:
            self.reg_proj.import_records(data)
        exc = cm.exception
        self.assertIn('error', exc.args[0])

    def is_good_csv(self, csv_string):
        "Helper to test csv strings"
        return isinstance(csv_string, basestring)

    @responses.activate
    def test_csv_export(self):
        """Test valid csv export """
        self.add_normalproject_response()
        csv = self.reg_proj.export_records(format='csv')
        self.assertTrue(self.is_good_csv(csv))

    @responses.activate
    def test_metadata_export(self):
        """Test valid metadata csv export"""
        self.add_normalproject_response()
        csv = self.reg_proj.export_metadata(format='csv')
        self.assertTrue(self.is_good_csv(csv))

    def test_bad_creds(self):
        "Test that exceptions are raised with bad URL or tokens"
        with self.assertRaises(RedcapError):
            Project(self.bad_url, self.reg_token)
        with self.assertRaises(RedcapError):
            Project(self.bad_url, '1')

    @responses.activate
    def test_fem_export(self):
        """ Test fem export in json format gives list of dicts"""
        self.add_long_project_response()
        fem = self.long_proj.export_fem(format='json')
        self.assertIsInstance(fem, list)
        for arm in fem:
            self.assertIsInstance(arm, dict)

    @responses.activate
    def test_file_export(self):
        """Test file export and proper content-type parsing"""
        self.add_normalproject_response()
        record, field = '1', 'file'
        #Upload first to make sure file is there
        self.import_file()
        # Now export it
        content, headers = self.reg_proj.export_file(record, field)
        self.assertIsInstance(content, basestring)
        # We should at least get the filename in the headers
        for key in ['name']:
            self.assertIn(key, headers)
        # needs to raise ValueError for exporting non-file fields
        with self.assertRaises(ValueError):
            self.reg_proj.export_file(record=record, field='dob')

    def import_file(self):
        upload_fname = self.upload_fname()
        with open(upload_fname, 'r') as fobj:
            response = self.reg_proj.import_file('1', 'file', upload_fname, fobj)
        return response

    def upload_fname(self):
        import os
        this_dir, this_fname = os.path.split(__file__)
        return os.path.join(this_dir, 'data.txt')

    @responses.activate
    def test_file_import(self):
        "Test file import"
        self.add_normalproject_response()
        # Make sure a well-formed request doesn't throw RedcapError
        try:
            response = self.import_file()
        except RedcapError:
            self.fail("Shouldn't throw RedcapError for successful imports")
        self.assertTrue('error' not in response)
        # Test importing a file to a non-file field raises a ValueError
        fname = self.upload_fname()
        with open(fname, 'r') as fobj:
            with self.assertRaises(ValueError):
                response = self.reg_proj.import_file('1', 'first_name',
                    fname, fobj)

    @responses.activate
    def test_file_delete(self):
        "Test file deletion"
        self.add_normalproject_response()
        # make sure deleting doesn't raise
        try:
            self.reg_proj.delete_file('1', 'file')
        except RedcapError:
            self.fail("Shouldn't throw RedcapError for successful deletes")

    @responses.activate
    def test_user_export(self):
        "Test user export"
        self.add_normalproject_response()
        users = self.reg_proj.export_users()
        # A project must have at least one user
        self.assertTrue(len(users) > 0)
        req_keys = ['firstname', 'lastname', 'email', 'username',
                    'expiration', 'data_access_group', 'data_export',
                    'forms']
        for user in users:
            for key in req_keys:
                self.assertIn(key, user)

    def test_verify_ssl(self):
        """Test argument making for SSL verification"""
        # Test we won't verify SSL cert for non-verified project
        post_kwargs = self.ssl_proj._kwargs()
        self.assertIn('verify', post_kwargs)
        self.assertFalse(post_kwargs['verify'])
        # Test we do verify SSL cert in normal project
        post_kwargs = self.reg_proj._kwargs()
        self.assertIn('verify', post_kwargs)
        self.assertTrue(post_kwargs['verify'])

    @responses.activate
    def test_export_data_access_groups(self):
        """Test we get 'redcap_data_access_group' in exported data"""
        self.add_normalproject_response()
        records = self.reg_proj.export_records(export_data_access_groups=True)
        for record in records:
            self.assertIn('redcap_data_access_group', record)
        # When not passed, that key shouldn't be there
        records = self.reg_proj.export_records()
        for record in records:
            self.assertNotIn('redcap_data_access_group', record)

    @responses.activate
    def test_export_survey_fields(self):
        """Test that we get the appropriate survey keys in the exported
        data.

        Note that the 'demographics' form has been setup as the survey
        in the `survey_proj` project. The _timestamp field will vary for
        users as their survey form will be named differently"""
        self.add_survey_project()
        self.add_normalproject_response()
        records = self.survey_proj.export_records(export_survey_fields=True)
        for record in records:
            self.assertIn('redcap_survey_identifier', record)
            self.assertIn('demographics_timestamp', record)
        # The regular project doesn't have a survey setup. Users should
        # be able this argument as True but it winds up a no-op.
        records = self.reg_proj.export_records(export_survey_fields=True)
        for record in records:
            self.assertNotIn('redcap_survey_identifier', record)
            self.assertNotIn('demographics_timestamp', record)

    @unittest.skipIf(skip_pd, "Couldn't import pandas")
    @responses.activate
    def test_metadata_to_df(self):
        """Test metadata export --> DataFrame"""
        self.add_normalproject_response()
        df = self.reg_proj.export_metadata(format='df')
        self.assertIsInstance(df, pd.DataFrame)

    @unittest.skipIf(skip_pd, "Couldn't import pandas")
    @responses.activate
    def test_export_to_df(self):
        """Test export --> DataFrame"""
        self.add_normalproject_response()
        self.add_long_project_response()
        df = self.reg_proj.export_records(format='df')
        self.assertIsInstance(df, pd.DataFrame)
        # Test it's a normal index
        self.assertTrue(hasattr(df.index, 'name'))
        # Test for a MultiIndex on longitudinal df
        long_df = self.long_proj.export_records(format='df', event_name='raw')
        self.assertTrue(hasattr(long_df.index, 'names'))

    @unittest.skipIf(skip_pd, "Couldn't import pandas")
    @responses.activate
    def test_export_df_kwargs(self):
        """Test passing kwargs to export DataFrame construction"""
        self.add_normalproject_response()
        df = self.reg_proj.export_records(format='df',
            df_kwargs={'index_col': 'first_name'})
        self.assertEqual(df.index.name, 'first_name')
        self.assertTrue('study_id' in df)

    @unittest.skipIf(skip_pd, "Couldn't import pandas")
    @responses.activate
    def test_metadata_df_kwargs(self):
        """Test passing kwargs to metadata DataFrame construction"""
        self.add_normalproject_response()
        df = self.reg_proj.export_metadata(format='df',
            df_kwargs={'index_col': 'field_label'})
        self.assertEqual(df.index.name, 'field_label')
        self.assertTrue('field_name' in df)

    @unittest.skipIf(skip_pd, "Couldn't import pandas")
    @responses.activate
    def test_import_dataframe(self):
        """Test importing a pandas.DataFrame"""
        self.add_normalproject_response()
        self.add_long_project_response()
        df = self.reg_proj.export_records(format='df')
        response = self.reg_proj.import_records(df)
        self.assertIn('count', response)
        self.assertNotIn('error', response)
        long_df = self.long_proj.export_records(event_name='raw', format='df')
        response = self.long_proj.import_records(long_df)
        self.assertIn('count', response)
        self.assertNotIn('error', response)

    @responses.activate
    def test_date_formatting(self):
        """Test date_format parameter"""
        self.add_normalproject_response()

        def import_factory(date_string):
            return [{'study_id': '1',
                     'dob': date_string}]

        # Default YMD with dashes
        import_ymd = import_factory('2000-01-01')
        response = self.reg_proj.import_records(import_ymd)
        self.assertEqual(response['count'], 1)

        # DMY with /
        import_dmy = import_factory('31/01/2000')
        response = self.reg_proj.import_records(import_dmy, date_format='DMY')
        self.assertEqual(response['count'], 1)

        import_mdy = import_factory('12/31/2000')
        response = self.reg_proj.import_records(import_mdy, date_format='MDY')
        self.assertEqual(response['count'], 1)

    def test_get_version(self):
        """Testing retrieval of REDCap version associated with Project"""
        self.assertTrue(isinstance(semantic_version.Version('1.0.0'), type(self.long_proj.redcap_version)))

    @responses.activate
    def test_export_checkbox_labels(self):
        """Testing the export of checkbox labels as field values"""
        self.add_normalproject_response()
        self.assertEqual(
            self.reg_proj.export_records(
                raw_or_label='label',
                export_checkbox_labels=True)[0]['matcheck1___1'],
                'Foo'
        )

    @responses.activate
    def test_export_always_include_def_field(self):
        """ Ensure def_field always comes in the output even if not explicity
        given in a requested form """
        self.add_normalproject_response()
        # If we just ask for a form, must also get def_field in there
        records = self.reg_proj.export_records(forms=['imaging'])
        for record in records:
            self.assertIn(self.reg_proj.def_field, record)
        # , still need it def_field even if not asked for in form and fields
        records = self.reg_proj.export_records(forms=['imaging'], fields=['foo_score'])
        for record in records:
            self.assertIn(self.reg_proj.def_field, record)
        # If we just ask for some fields, still need def_field
        records = self.reg_proj.export_records(fields=['foo_score'])
        for record in records:
            self.assertIn(self.reg_proj.def_field, record)
Esempio n. 2
0
class ProjectTests(unittest.TestCase):
    """docstring for ProjectTests"""

    long_proj_url = "https://redcap.longproject.edu/api/"
    normal_proj_url = "https://redcap.normalproject.edu/api/"
    ssl_proj_url = "https://redcap.sslproject.edu/api/"
    survey_proj_url = "https://redcap.surveyproject.edu/api/"
    bad_url = "https://redcap.badproject.edu/api"
    reg_token = "supersecrettoken"

    def setUp(self):
        self.create_projects()

    def tearDown(self):
        pass

    def add_long_project_response(self):
        def request_callback_long(request):
            parsed = urlparse.urlparse("?{}".format(request.body))
            data = urlparse.parse_qs(parsed.query)
            headers = {"Content-Type": "application/json"}

            request_type = data["content"][0]

            if "returnContent" in data:
                resp = {"count": 1}

            elif request_type == "metadata":
                resp = [{
                    "field_name": "record_id",
                    "field_label": "Record ID",
                    "form_name": "Test Form",
                    "arm_num": 1,
                    "name": "test",
                }]
            elif request_type == "version":
                resp = b"8.6.0"
                headers = {"content-type": "text/csv; charset=utf-8"}
                return (201, headers, resp)
            elif request_type == "event":
                resp = [{"unique_event_name": "raw"}]
            elif request_type == "arm":
                resp = [{"arm_num": 1, "name": "test"}]
            elif request_type in ["record", "formEventMapping"]:
                if "csv" in data["format"]:
                    resp = "record_id,test,redcap_event_name\n1,1,raw"
                    headers = {"content-type": "text/csv; charset=utf-8"}
                    return (201, headers, resp)

                resp = [{"field_name": "record_id"}, {"field_name": "test"}]

            return (201, headers, json.dumps(resp))

        responses.add_callback(
            responses.POST,
            self.long_proj_url,
            callback=request_callback_long,
            content_type="application/json",
        )

    # pylint: disable=too-many-branches
    def add_normalproject_response(self):
        def request_callback_normal(request):
            parsed = urlparse.urlparse("?{}".format(request.body))
            data = urlparse.parse_qs(parsed.query)
            headers = {"Content-Type": "application/json"}

            resp = None

            if " filename" in data:
                resp = {}
            else:
                request_type = data.get("content", ["unknown"])[0]

                if "returnContent" in data:
                    if "non_existent_key" in data["data"][0]:
                        resp = {"error": "invalid field"}
                    else:
                        resp = {"count": 1}
                elif request_type == "metadata":
                    if "csv" in data["format"]:
                        resp = (
                            "field_name,field_label,form_name,arm_num,name\n"
                            "record_id,Record ID,Test Form,1,test\n")
                        headers = {"content-type": "text/csv; charset=utf-8"}
                        return (201, headers, resp)

                    resp = [
                        {
                            "field_name": "record_id",
                            "field_label": "Record ID",
                            "form_name": "Test Form",
                            "arm_num": 1,
                            "name": "test",
                            "field_type": "text",
                        },
                        {
                            "field_name": "file",
                            "field_label": "File",
                            "form_name": "Test Form",
                            "arm_num": 1,
                            "name": "file",
                            "field_type": "file",
                        },
                        {
                            "field_name": "dob",
                            "field_label": "Date of Birth",
                            "form_name": "Test Form",
                            "arm_num": 1,
                            "name": "dob",
                            "field_type": "date",
                        },
                    ]
                elif request_type == "version":
                    resp = {"error": "no version info"}
                elif request_type == "event":
                    resp = {"error": "no events"}
                elif request_type == "arm":
                    resp = {"error": "no arm"}
                elif request_type == "record":
                    if "csv" in data["format"]:
                        resp = "record_id,test,first_name,study_id\n1,1,Peter,1"
                        headers = {"content-type": "text/csv; charset=utf-8"}
                        return (201, headers, resp)

                    if "exportDataAccessGroups" in data:
                        resp = [
                            {
                                "field_name": "record_id",
                                "redcap_data_access_group": "group1",
                            },
                            {
                                "field_name": "test",
                                "redcap_data_access_group": "group1",
                            },
                        ]
                    elif "label" in data.get("rawOrLabel"):
                        resp = [{"matcheck1___1": "Foo"}]
                    else:
                        resp = [
                            {
                                "record_id": "1",
                                "test": "test1"
                            },
                            {
                                "record_id": "2",
                                "test": "test"
                            },
                        ]
                elif request_type == "file":
                    resp = {}
                    headers["content-type"] = "text/plain;name=data.txt"
                elif request_type == "user":
                    resp = [{
                        "firstname": "test",
                        "lastname": "test",
                        "email": "test",
                        "username": "******",
                        "expiration": "test",
                        "data_access_group": "test",
                        "data_export": "test",
                        "forms": "test",
                    }]
                elif request_type == "generateNextRecordName":
                    resp = 123
                elif request_type == "project":
                    resp = {"project_id": 123}

                self.assertIsNotNone(
                    resp,
                    msg="No response for request_type '{}'".format(
                        request_type))

            return (201, headers, json.dumps(resp))

        responses.add_callback(
            responses.POST,
            self.normal_proj_url,
            callback=request_callback_normal,
            content_type="application/json",
        )

    # pylint: enable=too-many-branches

    def add_ssl_project(self):
        def request_callback_ssl(request):
            parsed = urlparse.urlparse("?{}".format(request.body))
            data = urlparse.parse_qs(parsed.query)

            request_type = data["content"][0]
            if request_type == "metadata":
                resp = [{
                    "field_name": "record_id",
                    "field_label": "Record ID",
                    "form_name": "Test Form",
                    "arm_num": 1,
                    "name": "test",
                }]
            if request_type == "version":
                resp = {"error": "no version info"}
            if request_type == "event":
                resp = {"error": "no events"}
            if request_type == "arm":
                resp = {"error": "no arm"}

            headers = {"Content-Type": "application/json"}
            return (201, headers, json.dumps(resp))

        responses.add_callback(
            responses.POST,
            self.ssl_proj_url,
            callback=request_callback_ssl,
            content_type="application/json",
        )

    def add_survey_project(self):
        def request_callback_survey(request):
            parsed = urlparse.urlparse("?{}".format(request.body))
            data = urlparse.parse_qs(parsed.query)

            request_type = data["content"][0]
            if request_type == "metadata":
                resp = [{
                    "field_name": "record_id",
                    "field_label": "Record ID",
                    "form_name": "Test Form",
                    "arm_num": 1,
                    "name": "test",
                }]
            elif request_type == "version":
                resp = {"error": "no version info"}
            elif request_type == "event":
                resp = {"error": "no events"}
            elif request_type == "arm":
                resp = {"error": "no arm"}
            elif request_type == "record":
                resp = [
                    {
                        "field_name": "record_id",
                        "redcap_survey_identifier": "test",
                        "demographics_timestamp": "a_real_date",
                    },
                    {
                        "field_name": "test",
                        "redcap_survey_identifier": "test",
                        "demographics_timestamp": "a_real_date",
                    },
                ]

            headers = {"Content-Type": "application/json"}
            return (201, headers, json.dumps(resp))

        responses.add_callback(
            responses.POST,
            self.survey_proj_url,
            callback=request_callback_survey,
            content_type="application/json",
        )

    @responses.activate
    def create_projects(self):
        self.add_long_project_response()
        self.add_normalproject_response()
        self.add_ssl_project()
        self.add_survey_project()

        self.long_proj = Project(self.long_proj_url, self.reg_token)
        self.reg_proj = Project(self.normal_proj_url, self.reg_token)
        self.ssl_proj = Project(self.ssl_proj_url,
                                self.reg_token,
                                verify_ssl=False)
        self.survey_proj = Project(self.survey_proj_url, self.reg_token)

    def test_good_init(self):
        """Ensure basic instantiation """

        self.assertIsInstance(self.long_proj, Project)
        self.assertIsInstance(self.reg_proj, Project)
        self.assertIsInstance(self.ssl_proj, Project)

    def test_normal_attrs(self):
        """Ensure projects are created with all normal attrs"""

        for attr in (
                "metadata",
                "field_names",
                "field_labels",
                "forms",
                "events",
                "arm_names",
                "arm_nums",
                "def_field",
        ):
            self.assertTrue(hasattr(self.reg_proj, attr))

    def test_long_attrs(self):
        "proj.events/arm_names/arm_nums should not be empty in long projects"

        self.assertIsNotNone(self.long_proj.events)
        self.assertIsNotNone(self.long_proj.arm_names)
        self.assertIsNotNone(self.long_proj.arm_nums)

    def test_is_longitudinal(self):
        "Test the is_longitudinal method"
        self.assertFalse(self.reg_proj.is_longitudinal())
        self.assertTrue(self.long_proj.is_longitudinal())

    def test_regular_attrs(self):
        """proj.events/arm_names/arm_nums should be empty tuples"""
        for attr in "events", "arm_names", "arm_nums":
            attr_obj = getattr(self.reg_proj, attr)
            self.assertIsNotNone(attr_obj)
            self.assertEqual(len(attr_obj), 0)

    @responses.activate
    def test_json_export(self):
        """ Make sure we get a list of dicts"""
        self.add_normalproject_response()
        data = self.reg_proj.export_records()
        self.assertIsInstance(data, list)
        for record in data:
            self.assertIsInstance(record, dict)

    @responses.activate
    def test_long_export(self):
        """After determining a unique event name, make sure we get a
        list of dicts"""
        self.add_long_project_response()
        unique_event = self.long_proj.events[0]["unique_event_name"]
        data = self.long_proj.export_records(events=[unique_event])
        self.assertIsInstance(data, list)
        for record in data:
            self.assertIsInstance(record, dict)

    @responses.activate
    def test_import_records(self):
        "Test record import"
        self.add_normalproject_response()
        data = self.reg_proj.export_records()
        response = self.reg_proj.import_records(data)
        self.assertIn("count", response)
        self.assertNotIn("error", response)

    @responses.activate
    def test_import_exception(self):
        "Test record import throws RedcapError for bad import"
        self.add_normalproject_response()
        data = self.reg_proj.export_records()
        data[0]["non_existent_key"] = "foo"
        with self.assertRaises(RedcapError) as assert_context:
            self.reg_proj.import_records(data)
        exc = assert_context.exception
        self.assertIn("error", exc.args[0])

    @responses.activate
    def test_import_metadata(self):
        "Test metadata import"
        self.add_normalproject_response()
        data = self.reg_proj.export_metadata()
        response = self.reg_proj.import_metadata(data)
        for field_dict in response:
            for key in [
                    "field_name", "field_label", "form_name", "arm_num", "name"
            ]:
                self.assertIn(key, field_dict)
            self.assertNotIn("error", response)

    @unittest.skip("Fails on test server for unknown reason")
    @responses.activate
    def test_import_reduced_metadata(self):
        "Test import of a reduced set of metadata"
        self.add_normalproject_response()
        original_data = self.reg_proj.export_metadata()
        # reducing the metadata
        reduced_data = original_data[0:1]
        imported_data = self.reg_proj.import_metadata(reduced_data)

        self.assertEqual(len(imported_data), len(reduced_data))

    @staticmethod
    def is_good_csv(csv_string):
        "Helper to test csv strings"
        return is_str(csv_string)

    @responses.activate
    def test_csv_export(self):
        """Test valid csv export """
        self.add_normalproject_response()
        csv = self.reg_proj.export_records(format="csv")
        self.assertTrue(self.is_good_csv(csv))

    @responses.activate
    def test_metadata_export(self):
        """Test valid metadata csv export"""
        self.add_normalproject_response()
        csv = self.reg_proj.export_metadata(format="csv")
        self.assertTrue(self.is_good_csv(csv))

    def test_metadata_export_passes_filters_as_arrays(self):
        self.reg_proj._call_api = mock.Mock()
        self.reg_proj._call_api.return_value = (None, None)
        self.reg_proj.export_metadata(
            fields=["field0", "field1", "field2"],
            forms=["form0", "form1", "form2"],
        )

        args, _ = self.reg_proj._call_api.call_args

        payload = args[0]

        self.assertEqual(payload["fields[0]"], "field0")
        self.assertEqual(payload["fields[1]"], "field1")
        self.assertEqual(payload["fields[2]"], "field2")
        self.assertEqual(payload["forms[2]"], "form2")

    def test_bad_creds(self):
        "Test that exceptions are raised with bad URL or tokens"
        with self.assertRaises(RedcapError):
            Project(self.bad_url, self.reg_token)
        with self.assertRaises(RedcapError):
            Project(self.bad_url, "1")

    @responses.activate
    def test_fem_export(self):
        """ Test fem export in json format gives list of dicts"""
        self.add_long_project_response()
        fem = self.long_proj.export_fem(format="json")
        self.assertIsInstance(fem, list)
        for arm in fem:
            self.assertIsInstance(arm, dict)

    def test_fem_export_passes_filters_as_arrays(self):
        self.reg_proj._call_api = mock.Mock()
        self.reg_proj._call_api.return_value = (None, None)
        self.reg_proj.export_fem(arms=["arm0", "arm1", "arm2"], )

        args, _ = self.reg_proj._call_api.call_args

        payload = args[0]

        self.assertEqual(payload["arms[0]"], "arm0")
        self.assertEqual(payload["arms[1]"], "arm1")
        self.assertEqual(payload["arms[2]"], "arm2")

    @unittest.skipIf(LOCAL_DEV, "Fails on local server for unknown reason")
    @responses.activate
    def test_file_export(self):
        """Test file export and proper content-type parsing"""
        self.add_normalproject_response()
        record, field = "1", "file"
        # Upload first to make sure file is there
        self.import_file()
        # Now export it
        content, headers = self.reg_proj.export_file(record, field)
        self.assertTrue(is_bytestring(content))
        # We should at least get the filename in the headers
        for key in ["name"]:
            self.assertIn(key, headers)
        # needs to raise ValueError for exporting non-file fields
        with self.assertRaises(ValueError):
            self.reg_proj.export_file(record=record, field="dob")

    def import_file(self):
        upload_fname = self.upload_fname()
        with open(upload_fname, "r") as fobj:
            response = self.reg_proj.import_file("1", "file", upload_fname,
                                                 fobj)
        return response

    @staticmethod
    def upload_fname():
        this_dir, _ = os.path.split(__file__)
        return os.path.join(this_dir, "data.txt")

    @unittest.skipIf(LOCAL_DEV, "Fails on local server for unknown reason")
    @responses.activate
    def test_file_import(self):
        "Test file import"
        self.add_normalproject_response()
        # Make sure a well-formed request doesn't throw RedcapError
        try:
            response = self.import_file()
        except RedcapError:
            self.fail("Shouldn't throw RedcapError for successful imports")
        self.assertTrue("error" not in response)
        # Test importing a file to a non-file field raises a ValueError
        fname = self.upload_fname()
        with open(fname, "r") as fobj:
            with self.assertRaises(ValueError):
                response = self.reg_proj.import_file("1", "first_name", fname,
                                                     fobj)

    @responses.activate
    def test_file_delete(self):
        "Test file deletion"
        self.add_normalproject_response()
        # make sure deleting doesn't raise
        try:
            self.reg_proj.delete_file("1", "file")
        except RedcapError:
            self.fail("Shouldn't throw RedcapError for successful deletes")

    @responses.activate
    def test_user_export(self):
        "Test user export"
        self.add_normalproject_response()
        users = self.reg_proj.export_users()
        # A project must have at least one user
        self.assertTrue(len(users) > 0)
        req_keys = [
            "firstname",
            "lastname",
            "email",
            "username",
            "expiration",
            "data_access_group",
            "data_export",
            "forms",
        ]
        for user in users:
            for key in req_keys:
                self.assertIn(key, user)

    def test_verify_ssl(self):
        """Test argument making for SSL verification"""
        # Test we won't verify SSL cert for non-verified project
        post_kwargs = self.ssl_proj._kwargs()
        self.assertIn("verify", post_kwargs)
        self.assertFalse(post_kwargs["verify"])
        # Test we do verify SSL cert in normal project
        post_kwargs = self.reg_proj._kwargs()
        self.assertIn("verify", post_kwargs)
        self.assertTrue(post_kwargs["verify"])

    @responses.activate
    def test_export_data_access_groups(self):
        """Test we get 'redcap_data_access_group' in exported data"""
        self.add_normalproject_response()
        records = self.reg_proj.export_records(export_data_access_groups=True)
        for record in records:
            self.assertIn("redcap_data_access_group", record)
        # When not passed, that key shouldn't be there
        records = self.reg_proj.export_records()
        for record in records:
            self.assertNotIn("redcap_data_access_group", record)

    @responses.activate
    def test_export_survey_fields(self):
        """Test that we get the appropriate survey keys in the exported
        data.

        Note that the 'demographics' form has been setup as the survey
        in the `survey_proj` project. The _timestamp field will vary for
        users as their survey form will be named differently"""
        self.add_survey_project()
        self.add_normalproject_response()
        records = self.survey_proj.export_records(export_survey_fields=True)
        for record in records:
            self.assertIn("redcap_survey_identifier", record)
            self.assertIn("demographics_timestamp", record)
        # The regular project doesn't have a survey setup. Users should
        # be able this argument as True but it winds up a no-op.
        records = self.reg_proj.export_records(export_survey_fields=True)
        for record in records:
            self.assertNotIn("redcap_survey_identifier", record)
            self.assertNotIn("demographics_timestamp", record)

    @unittest.skipIf(SKIP_PD, "Couldn't import pandas")
    @responses.activate
    def test_metadata_to_df(self):
        """Test metadata export --> DataFrame"""
        self.add_normalproject_response()
        dataframe = self.reg_proj.export_metadata(format="df")
        self.assertIsInstance(dataframe, pd.DataFrame)

    @unittest.skipIf(SKIP_PD, "Couldn't import pandas")
    @responses.activate
    def test_export_to_df(self):
        """Test export --> DataFrame"""
        self.add_normalproject_response()
        self.add_long_project_response()
        dataframe = self.reg_proj.export_records(format="df")
        self.assertIsInstance(dataframe, pd.DataFrame)
        # Test it's a normal index
        self.assertTrue(hasattr(dataframe.index, "name"))
        # Test for a MultiIndex on longitudinal df
        long_dataframe = self.long_proj.export_records(format="df",
                                                       event_name="raw")
        self.assertTrue(hasattr(long_dataframe.index, "names"))

    @unittest.skipIf(SKIP_PD, "Couldn't import pandas")
    @responses.activate
    def test_export_df_kwargs(self):
        """Test passing kwargs to export DataFrame construction"""
        self.add_normalproject_response()
        dataframe = self.reg_proj.export_records(
            format="df", df_kwargs={"index_col": "first_name"})
        self.assertEqual(dataframe.index.name, "first_name")
        self.assertTrue("study_id" in dataframe)

    @unittest.skipIf(SKIP_PD, "Couldn't import pandas")
    @responses.activate
    def test_metadata_df_kwargs(self):
        """Test passing kwargs to metadata DataFrame construction"""
        self.add_normalproject_response()
        dataframe = self.reg_proj.export_metadata(
            format="df", df_kwargs={"index_col": "field_label"})
        self.assertEqual(dataframe.index.name, "field_label")
        self.assertTrue("field_name" in dataframe)

    @unittest.skipIf(SKIP_PD, "Couldn't import pandas")
    @responses.activate
    def test_import_dataframe(self):
        """Test importing a pandas.DataFrame"""
        self.add_normalproject_response()
        self.add_long_project_response()
        dataframe = self.reg_proj.export_records(format="df")
        response = self.reg_proj.import_records(dataframe)
        self.assertIn("count", response)
        self.assertNotIn("error", response)
        long_dataframe = self.long_proj.export_records(event_name="raw",
                                                       format="df")
        response = self.long_proj.import_records(long_dataframe)
        self.assertIn("count", response)
        self.assertNotIn("error", response)

    def test_export_records_handles_empty_data_error(self):
        self.reg_proj._call_api = mock.Mock()
        self.reg_proj._call_api.return_value = "\n", {}
        dataframe = self.reg_proj.export_records(format="df")
        self.assertTrue(dataframe.empty)

    def test_export_fem_handles_empty_data_error(self):
        self.reg_proj._call_api = mock.Mock()
        self.reg_proj._call_api.return_value = "\n", {}
        dataframe = self.reg_proj.export_fem(format="df")
        self.assertTrue(dataframe.empty)

    def test_export_metadata_handles_empty_data_error(self):
        self.reg_proj._call_api = mock.Mock()
        self.reg_proj._call_api.return_value = "\n", {}
        dataframe = self.reg_proj.export_metadata(format="df")
        self.assertTrue(dataframe.empty)

    @responses.activate
    def test_date_formatting(self):
        """Test date_format parameter"""
        self.add_normalproject_response()

        def import_factory(date_string):
            return [{"study_id": "1", "dob": date_string}]

        # Default YMD with dashes
        import_ymd = import_factory("2000-01-01")
        response = self.reg_proj.import_records(import_ymd)
        self.assertEqual(response["count"], 1)

        # DMY with /
        import_dmy = import_factory("31/01/2000")
        response = self.reg_proj.import_records(import_dmy, date_format="DMY")
        self.assertEqual(response["count"], 1)

        import_mdy = import_factory("12/31/2000")
        response = self.reg_proj.import_records(import_mdy, date_format="MDY")
        self.assertEqual(response["count"], 1)

    def test_get_version(self):
        """Testing retrieval of REDCap version associated with Project"""
        self.assertTrue(
            isinstance(semantic_version.Version("1.0.0"),
                       type(self.long_proj.redcap_version)))

    @responses.activate
    def test_export_checkbox_labels(self):
        """Testing the export of checkbox labels as field values"""
        self.add_normalproject_response()
        self.assertEqual(
            self.reg_proj.export_records(
                raw_or_label="label",
                export_checkbox_labels=True)[0]["matcheck1___1"],
            "Foo",
        )

    @responses.activate
    def test_export_always_include_def_field(self):
        """Ensure def_field always comes in the output even if not explicity
        given in a requested form"""
        self.add_normalproject_response()
        # If we just ask for a form, must also get def_field in there
        records = self.reg_proj.export_records(forms=["imaging"])
        for record in records:
            self.assertIn(self.reg_proj.def_field, record)
        # , still need it def_field even if not asked for in form and fields
        records = self.reg_proj.export_records(forms=["imaging"],
                                               fields=["foo_score"])
        for record in records:
            self.assertIn(self.reg_proj.def_field, record)
        # If we just ask for some fields, still need def_field
        records = self.reg_proj.export_records(fields=["foo_score"])
        for record in records:
            self.assertIn(self.reg_proj.def_field, record)

    def test_export_passes_filters_as_arrays(self):
        self.reg_proj._call_api = mock.Mock()
        self.reg_proj._call_api.return_value = (None, None)
        self.reg_proj.export_records(
            records=["record0", "record1", "record2"],
            fields=["field0", "field1", "field2"],
            forms=["form0", "form1", "form2"],
            events=["event0", "event1", "event2"],
        )

        args, _ = self.reg_proj._call_api.call_args

        payload = args[0]

        self.assertEqual(payload["records[0]"], "record0")
        self.assertEqual(payload["records[1]"], "record1")
        self.assertEqual(payload["records[2]"], "record2")
        self.assertEqual(payload["fields[1]"], "field1")
        self.assertEqual(payload["forms[2]"], "form2")
        self.assertEqual(payload["events[0]"], "event0")

    @responses.activate
    def test_generate_next_record_name(self):
        "Test exporting the next potential record ID for a project"
        self.add_normalproject_response()

        next_name = self.reg_proj.generate_next_record_name()

        self.assertEqual(next_name, 123)

    @responses.activate
    def test_export_project_info(self):
        "Test export of project information"
        self.add_normalproject_response()

        info = self.reg_proj.export_project_info()

        self.assertEqual(info["project_id"], 123)
Esempio n. 3
0
class ProjectTests(unittest.TestCase):
    """docstring for ProjectTests"""

    def setUp(self):
        self.url = 'https://redcap.vanderbilt.edu/api/'
        self.long_proj = Project(self.url, '1387872621BBF1C17CC47FD8AE25FF54')
        self.reg_proj = Project(self.url, '8E66DB6844D58E990075AFB51658A002')
        self.ssl_proj = Project(self.url, '8E66DB6844D58E990075AFB51658A002',
            verify_ssl=False)

    def tearDown(self):
        pass

    def test_good_init(self):
        """Ensure basic instantiation """
        self.assertIsInstance(self.long_proj, Project)
        self.assertIsInstance(self.reg_proj, Project)
        self.assertIsInstance(self.ssl_proj, Project)

    def test_normal_attrs(self):
        """Ensure projects are created with all normal attrs"""
        for attr in ('metadata', 'field_names', 'field_labels', 'forms',
            'events', 'arm_names', 'arm_nums', 'def_field'):
            self.assertTrue(hasattr(self.reg_proj, attr))

    def test_long_attrs(self):
        "proj.events/arm_names/arm_nums should not be empty in long projects"
        self.assertIsNotNone(self.long_proj.events)
        self.assertIsNotNone(self.long_proj.arm_names)
        self.assertIsNotNone(self.long_proj.arm_nums)

    def test_is_longitudinal(self):
        "Test the is_longitudinal method"
        self.assertFalse(self.reg_proj.is_longitudinal())
        self.assertTrue(self.long_proj.is_longitudinal())

    def test_regular_attrs(self):
        """proj.events/arm_names/arm_nums should be empty tuples"""
        for attr in 'events', 'arm_names', 'arm_nums':
            attr_obj = getattr(self.reg_proj, attr)
            self.assertIsNotNone(attr_obj)
            self.assertEqual(len(attr_obj), 0)

    def test_obj_export(self):
        """ Make sure we get a list of dicts"""
        data = self.reg_proj.export_records()
        self.assertIsInstance(data, list)
        for record in data:
            self.assertIsInstance(record, dict)

    def test_long_export(self):
        """After determining a unique event name, make sure we get a
        list of dicts"""
        unique_event = self.long_proj.events[0]['unique_event_name']
        data = self.long_proj.export_records(events=[unique_event])
        self.assertIsInstance(data, list)
        for record in data:
            self.assertIsInstance(record, dict)

    def is_good_csv(self, csv_string):
        "Helper to test csv strings"
        return isinstance(csv_string, basestring)

    def test_csv_export(self):
        """Test valid csv export """
        csv = self.reg_proj.export_records(format='csv')
        self.assertTrue(self.is_good_csv(csv))

    def test_metadata_export(self):
        """Test valid metadata csv export"""
        csv = self.reg_proj.export_metadata(format='csv')
        self.assertTrue(self.is_good_csv(csv))

    def test_fem_export(self):
        """ Test fem export in obj format gives list of dicts"""
        fem = self.long_proj.export_fem(format='obj')
        self.assertIsInstance(fem, list)
        for arm in fem:
            self.assertIsInstance(arm, dict)

    def test_file_export(self):
        """Test file export and proper content-type parsing"""
        record, field = '1', 'file'
        #Upload first to make sure file is there
        self.import_file()
        # Now export it
        content, headers = self.reg_proj.export_file(record, field)
        self.assertIsInstance(content, basestring)
        # We should at least get the filename in the headers
        for key in ['name']:
            self.assertIn(key, headers)
        # needs to raise ValueError for exporting non-file fields
        with self.assertRaises(ValueError):
            self.reg_proj.export_file(record=record, field='dob')
        # Delete and make sure we get an RedcapError with next export
        self.reg_proj.delete_file(record, field)
        with self.assertRaises(RedcapError):
            self.reg_proj.export_file(record, field)

    def import_file(self):
        upload_fname = self.upload_fname()
        with open(upload_fname, 'r') as fobj:
            response = self.reg_proj.import_file('1', 'file', upload_fname, fobj)
        return response

    def upload_fname(self):
        import os
        this_dir, this_fname = os.path.split(__file__)
        return os.path.join(this_dir, 'data.txt')

    def test_file_import(self):
        "Test file import"
        # Make sure a well-formed request doesn't throw RedcapError
        try:
            response = self.import_file()
        except RedcapError:
            self.fail("Shouldn't throw RedcapError for successful imports")
        self.assertTrue('error' not in response)
        # Test importing a file to a non-file field raises a ValueError
        fname = self.upload_fname()
        with open(fname, 'r') as fobj:
            with self.assertRaises(ValueError):
                response = self.reg_proj.import_file('1', 'first_name',
                    fname, fobj)

    def test_file_delete(self):
        "Test file deletion"
        # upload a file
        fname = self.upload_fname()
        with open(fname, 'r') as fobj:
            self.reg_proj.import_file('1', 'file', fname, fobj)
        # make sure deleting doesn't raise
        try:
            self.reg_proj.delete_file('1', 'file')
        except RedcapError:
            self.fail("Shouldn't throw RedcapError for successful deletes")

    def test_user_export(self):
        "Test user export"
        users = self.reg_proj.export_users()
        # A project must have at least one user
        self.assertTrue(len(users) > 0)
        req_keys = ['firstname', 'lastname', 'email', 'username',
                    'expiration', 'data_access_group', 'data_export',
                    'forms']
        for user in users:
            for key in req_keys:
                self.assertIn(key, user)

    def test_verify_ssl(self):
        """Test argument making for SSL verification"""
        # Test we won't verify SSL cert for non-verified project
        post_kwargs = self.ssl_proj._kwargs()
        self.assertIn('verify', post_kwargs)
        self.assertFalse(post_kwargs['verify'])
        # Test we do verify SSL cert in normal project
        post_kwargs = self.reg_proj._kwargs()
        self.assertIn('verify', post_kwargs)
        self.assertTrue(post_kwargs['verify'])


    @unittest.skipIf(skip_pd, "Couldn't import pandas")
    def test_metadata_to_df(self):
        """Test metadata export --> DataFrame"""
        df = self.reg_proj.export_metadata(format='df')
        self.assertIsInstance(df, pd.DataFrame)

    @unittest.skipIf(skip_pd, "Couldn't import pandas")
    def test_export_to_df(self):
        """Test export --> DataFrame"""
        df = self.reg_proj.export_records(format='df')
        self.assertIsInstance(df, pd.DataFrame)
        # Test it's a normal index
        self.assertTrue(hasattr(df.index, 'name'))
        # Test for a MultiIndex on longitudinal df
        long_df = self.long_proj.export_records(format='df', event_name='raw')
        self.assertTrue(hasattr(long_df.index, 'names'))

    @unittest.skipIf(skip_pd, "Couldn't import pandas")
    def test_export_df_kwargs(self):
        """Test passing kwargs to export DataFrame construction"""
        df = self.reg_proj.export_records(format='df',
            df_kwargs={'index_col': 'first_name'})
        self.assertEqual(df.index.name, 'first_name')
        self.assertTrue('study_id' in df)

    @unittest.skipIf(skip_pd, "Couldn't import pandas")
    def test_metadata_df_kwargs(self):
        """Test passing kwargs to metadata DataFrame construction"""
        df = self.reg_proj.export_metadata(format='df',
            df_kwargs={'index_col': 'field_label'})
        self.assertEqual(df.index.name, 'field_label')
        self.assertTrue('field_name' in df)

    @unittest.skipIf(skip_pd, "Couldn't import pandas")
    def test_import_dataframe(self):
        """Test importing a pandas.DataFrame"""
        df = self.reg_proj.export_records(format='df')
        response = self.reg_proj.import_records(df)
        self.assertIn('count', response)
        self.assertNotIn('error', response)
        long_df = self.long_proj.export_records(event_name='raw', format='df')
        response = self.long_proj.import_records(long_df)
        self.assertIn('count', response)
        self.assertNotIn('error', response)
Esempio n. 4
0
class ProjectTests(unittest.TestCase):
    """docstring for ProjectTests"""

    long_proj_url = 'https://redcap.longproject.edu/api/'
    normal_proj_url = 'https://redcap.normalproject.edu/api/'
    ssl_proj_url = 'https://redcap.sslproject.edu/api/'
    survey_proj_url = 'https://redcap.surveyproject.edu/api/'
    bad_url = 'https://redcap.badproject.edu/api'
    reg_token = 'supersecrettoken'

    def setUp(self):
        self.create_projects()

    def tearDown(self):
        pass

    def add_long_project_response(self):
        def request_callback_long(request):
            parsed = urlparse.urlparse("?{}".format(request.body))
            data = urlparse.parse_qs(parsed.query)
            headers = {"Content-Type": "application/json"}

            request_type = data["content"][0]

            if "returnContent" in data:
                resp = {"count": 1}

            elif (request_type == "metadata"):
                resp = [{
                    'field_name': 'record_id',
                    'field_label': 'Record ID',
                    'form_name': 'Test Form',
                    "arm_num": 1,
                    "name": "test"
                }]
            elif (request_type == "version"):
                resp = b'8.6.0'
                headers = {'content-type': 'text/csv; charset=utf-8'}
                return (201, headers, resp)
            elif (request_type == "event"):
                resp = [{
                    'unique_event_name': "raw"
                }]
            elif (request_type == "arm"):
                resp = [{
                    "arm_num": 1,
                    "name": "test"
                }]
            elif (request_type in ["record", "formEventMapping"]):
                if "csv" in data["format"]:
                    resp = "record_id,test,redcap_event_name\n1,1,raw"
                    headers = {'content-type': 'text/csv; charset=utf-8'}
                    return (201, headers, resp)
                else:
                    resp = [{"field_name":"record_id"}, {"field_name":"test"}]

            return (201, headers, json.dumps(resp))

        responses.add_callback(
            responses.POST,
            self.long_proj_url,
            callback=request_callback_long,
            content_type="application/json",
        )

    def add_normalproject_response(self):
        def request_callback_normal(request):
            parsed = urlparse.urlparse("?{}".format(request.body))
            data = urlparse.parse_qs(parsed.query)
            headers = {"Content-Type": "application/json"}

            if " filename" in data:
                resp = {}
            else:
                request_type = data.get("content", ['unknown'])[0]

                if "returnContent" in data:
                    if "non_existent_key" in data["data"][0]:
                        resp = {"error": "invalid field"}
                    else:
                        resp = {"count": 1}
                elif (request_type == "metadata"):
                    if "csv" in data["format"]:
                        resp = "field_name,field_label,form_name,arm_num,name\n"\
                            "record_id,Record ID,Test Form,1,test\n"
                        headers = {'content-type': 'text/csv; charset=utf-8'}
                        return (201, headers, resp)

                    else:
                        resp = [{
                            'field_name': 'record_id',
                            'field_label': 'Record ID',
                            'form_name': 'Test Form',
                            "arm_num": 1,
                            "name": "test",
                            "field_type": "text",
                        }, {
                            'field_name': 'file',
                            'field_label': 'File',
                            'form_name': 'Test Form',
                            "arm_num": 1,
                            "name": "file",
                            "field_type": "file",
                        }, {
                            'field_name': 'dob',
                            'field_label': 'Date of Birth',
                            'form_name': 'Test Form',
                            "arm_num": 1,
                            "name": "dob",
                            "field_type": "date",
                        }]
                elif (request_type == "version"):
                    resp = {
                        'error': "no version info"
                    }
                elif (request_type == "event"):
                    resp = {
                        'error': "no events"
                    }
                elif (request_type == "arm"):
                    resp = {
                        'error': "no arm"
                    }
                elif (request_type == "record"):
                    if "csv" in data["format"]:
                        resp = "record_id,test,first_name,study_id\n1,1,Peter,1"
                        headers = {'content-type': 'text/csv; charset=utf-8'}
                        return (201, headers, resp)
                    elif "exportDataAccessGroups" in data:
                        resp = [
                            {"field_name":"record_id", "redcap_data_access_group": "group1"},
                            {"field_name":"test", "redcap_data_access_group": "group1"}
                        ]
                    elif "label" in data.get("rawOrLabel"):
                        resp = [{"matcheck1___1": "Foo"}]
                    else:
                        resp = [
                            {"record_id": "1", "test": "test1"},
                            {"record_id": "2", "test": "test"}
                        ]
                elif (request_type == "file"):
                    resp = {}
                    headers["content-type"] = "text/plain;name=data.txt"
                elif (request_type == "user"):
                    resp = [
                        {
                            'firstname': "test",
                            'lastname': "test",
                            'email': "test",
                            'username': "******",
                            'expiration': "test",
                            'data_access_group': "test",
                            'data_export': "test",
                            'forms': "test"
                        }
                    ]

            return (201, headers, json.dumps(resp))

        responses.add_callback(
            responses.POST,
            self.normal_proj_url,
            callback=request_callback_normal,
            content_type="application/json",
        )

    def add_ssl_project(self):
        def request_callback_ssl(request):
            parsed = urlparse.urlparse("?{}".format(request.body))
            data = urlparse.parse_qs(parsed.query)

            request_type = data["content"][0]
            if (request_type == "metadata"):
                resp = [{
                    'field_name': 'record_id',
                    'field_label': 'Record ID',
                    'form_name': 'Test Form',
                    "arm_num": 1,
                    "name": "test"
                }]
            if (request_type == "version"):
                resp = {
                    'error': "no version info"
                }
            if (request_type == "event"):
                resp = {
                    'error': "no events"
                }
            if (request_type == "arm"):
                resp = {
                    'error': "no arm"
                }

            headers = {"Content-Type": "application/json"}
            return (201, headers, json.dumps(resp))

        responses.add_callback(
            responses.POST,
            self.ssl_proj_url,
            callback=request_callback_ssl,
            content_type="application/json",
        )

    def add_survey_project(self):
        def request_callback_survey(request):
            parsed = urlparse.urlparse("?{}".format(request.body))
            data = urlparse.parse_qs(parsed.query)

            request_type = data["content"][0]
            if (request_type == "metadata"):
                resp = [{
                    'field_name': 'record_id',
                    'field_label': 'Record ID',
                    'form_name': 'Test Form',
                    "arm_num": 1,
                    "name": "test"
                }]
            elif (request_type == "version"):
                resp = {
                    'error': "no version info"
                }
            elif (request_type == "event"):
                resp = {
                    'error': "no events"
                }
            elif (request_type == "arm"):
                resp = {
                    'error': "no arm"
                }
            elif (request_type == "record"):
                resp = [
                    {"field_name":"record_id", "redcap_survey_identifier": "test", "demographics_timestamp": "a_real_date"},
                    {"field_name":"test", "redcap_survey_identifier": "test", "demographics_timestamp": "a_real_date"}
                ]

            headers = {"Content-Type": "application/json"}
            return (201, headers, json.dumps(resp))

        responses.add_callback(
            responses.POST,
            self.survey_proj_url,
            callback=request_callback_survey,
            content_type="application/json",
        )

    @responses.activate
    def create_projects(self):
        self.add_long_project_response()
        self.add_normalproject_response()
        self.add_ssl_project()
        self.add_survey_project()

        self.long_proj = Project(self.long_proj_url, self.reg_token)
        self.reg_proj = Project(self.normal_proj_url, self.reg_token)
        self.ssl_proj = Project(self.ssl_proj_url, self.reg_token, verify_ssl=False)
        self.survey_proj = Project(self.survey_proj_url, self.reg_token)


    def test_good_init(self):
        """Ensure basic instantiation """

        self.assertIsInstance(self.long_proj, Project)
        self.assertIsInstance(self.reg_proj, Project)
        self.assertIsInstance(self.ssl_proj, Project)

    def test_normal_attrs(self):
        """Ensure projects are created with all normal attrs"""

        for attr in ('metadata', 'field_names', 'field_labels', 'forms',
            'events', 'arm_names', 'arm_nums', 'def_field'):
            self.assertTrue(hasattr(self.reg_proj, attr))

    def test_long_attrs(self):
        "proj.events/arm_names/arm_nums should not be empty in long projects"

        self.assertIsNotNone(self.long_proj.events)
        self.assertIsNotNone(self.long_proj.arm_names)
        self.assertIsNotNone(self.long_proj.arm_nums)

    def test_is_longitudinal(self):
        "Test the is_longitudinal method"
        self.assertFalse(self.reg_proj.is_longitudinal())
        self.assertTrue(self.long_proj.is_longitudinal())

    def test_regular_attrs(self):
        """proj.events/arm_names/arm_nums should be empty tuples"""
        for attr in 'events', 'arm_names', 'arm_nums':
            attr_obj = getattr(self.reg_proj, attr)
            self.assertIsNotNone(attr_obj)
            self.assertEqual(len(attr_obj), 0)

    @responses.activate
    def test_json_export(self):
        """ Make sure we get a list of dicts"""
        self.add_normalproject_response()
        data = self.reg_proj.export_records()
        self.assertIsInstance(data, list)
        for record in data:
            self.assertIsInstance(record, dict)

    @responses.activate
    def test_long_export(self):
        """After determining a unique event name, make sure we get a
        list of dicts"""
        self.add_long_project_response()
        unique_event = self.long_proj.events[0]['unique_event_name']
        data = self.long_proj.export_records(events=[unique_event])
        self.assertIsInstance(data, list)
        for record in data:
            self.assertIsInstance(record, dict)

    @responses.activate
    def test_import_records(self):
        "Test record import"
        self.add_normalproject_response()
        data = self.reg_proj.export_records()
        response = self.reg_proj.import_records(data)
        self.assertIn('count', response)
        self.assertNotIn('error', response)

    @responses.activate
    def test_import_exception(self):
        "Test record import throws RedcapError for bad import"
        self.add_normalproject_response()
        data = self.reg_proj.export_records()
        data[0]['non_existent_key'] = 'foo'
        with self.assertRaises(RedcapError) as cm:
            self.reg_proj.import_records(data)
        exc = cm.exception
        self.assertIn('error', exc.args[0])

    def is_good_csv(self, csv_string):
        "Helper to test csv strings"
        return is_str(csv_string)

    @responses.activate
    def test_csv_export(self):
        """Test valid csv export """
        self.add_normalproject_response()
        csv = self.reg_proj.export_records(format='csv')
        self.assertTrue(self.is_good_csv(csv))

    @responses.activate
    def test_metadata_export(self):
        """Test valid metadata csv export"""
        self.add_normalproject_response()
        csv = self.reg_proj.export_metadata(format='csv')
        self.assertTrue(self.is_good_csv(csv))

    def test_bad_creds(self):
        "Test that exceptions are raised with bad URL or tokens"
        with self.assertRaises(RedcapError):
            Project(self.bad_url, self.reg_token)
        with self.assertRaises(RedcapError):
            Project(self.bad_url, '1')

    @responses.activate
    def test_fem_export(self):
        """ Test fem export in json format gives list of dicts"""
        self.add_long_project_response()
        fem = self.long_proj.export_fem(format='json')
        self.assertIsInstance(fem, list)
        for arm in fem:
            self.assertIsInstance(arm, dict)

    @responses.activate
    def test_file_export(self):
        """Test file export and proper content-type parsing"""
        self.add_normalproject_response()
        record, field = '1', 'file'
        #Upload first to make sure file is there
        self.import_file()
        # Now export it
        content, headers = self.reg_proj.export_file(record, field)
        self.assertTrue(is_bytestring(content))
        # We should at least get the filename in the headers
        for key in ['name']:
            self.assertIn(key, headers)
        # needs to raise ValueError for exporting non-file fields
        with self.assertRaises(ValueError):
            self.reg_proj.export_file(record=record, field='dob')

    def import_file(self):
        upload_fname = self.upload_fname()
        with open(upload_fname, 'r') as fobj:
            response = self.reg_proj.import_file('1', 'file', upload_fname, fobj)
        return response

    def upload_fname(self):
        import os
        this_dir, this_fname = os.path.split(__file__)
        return os.path.join(this_dir, 'data.txt')

    @responses.activate
    def test_file_import(self):
        "Test file import"
        self.add_normalproject_response()
        # Make sure a well-formed request doesn't throw RedcapError
        try:
            response = self.import_file()
        except RedcapError:
            self.fail("Shouldn't throw RedcapError for successful imports")
        self.assertTrue('error' not in response)
        # Test importing a file to a non-file field raises a ValueError
        fname = self.upload_fname()
        with open(fname, 'r') as fobj:
            with self.assertRaises(ValueError):
                response = self.reg_proj.import_file('1', 'first_name',
                    fname, fobj)

    @responses.activate
    def test_file_delete(self):
        "Test file deletion"
        self.add_normalproject_response()
        # make sure deleting doesn't raise
        try:
            self.reg_proj.delete_file('1', 'file')
        except RedcapError:
            self.fail("Shouldn't throw RedcapError for successful deletes")

    @responses.activate
    def test_user_export(self):
        "Test user export"
        self.add_normalproject_response()
        users = self.reg_proj.export_users()
        # A project must have at least one user
        self.assertTrue(len(users) > 0)
        req_keys = ['firstname', 'lastname', 'email', 'username',
                    'expiration', 'data_access_group', 'data_export',
                    'forms']
        for user in users:
            for key in req_keys:
                self.assertIn(key, user)

    def test_verify_ssl(self):
        """Test argument making for SSL verification"""
        # Test we won't verify SSL cert for non-verified project
        post_kwargs = self.ssl_proj._kwargs()
        self.assertIn('verify', post_kwargs)
        self.assertFalse(post_kwargs['verify'])
        # Test we do verify SSL cert in normal project
        post_kwargs = self.reg_proj._kwargs()
        self.assertIn('verify', post_kwargs)
        self.assertTrue(post_kwargs['verify'])

    @responses.activate
    def test_export_data_access_groups(self):
        """Test we get 'redcap_data_access_group' in exported data"""
        self.add_normalproject_response()
        records = self.reg_proj.export_records(export_data_access_groups=True)
        for record in records:
            self.assertIn('redcap_data_access_group', record)
        # When not passed, that key shouldn't be there
        records = self.reg_proj.export_records()
        for record in records:
            self.assertNotIn('redcap_data_access_group', record)

    @responses.activate
    def test_export_survey_fields(self):
        """Test that we get the appropriate survey keys in the exported
        data.

        Note that the 'demographics' form has been setup as the survey
        in the `survey_proj` project. The _timestamp field will vary for
        users as their survey form will be named differently"""
        self.add_survey_project()
        self.add_normalproject_response()
        records = self.survey_proj.export_records(export_survey_fields=True)
        for record in records:
            self.assertIn('redcap_survey_identifier', record)
            self.assertIn('demographics_timestamp', record)
        # The regular project doesn't have a survey setup. Users should
        # be able this argument as True but it winds up a no-op.
        records = self.reg_proj.export_records(export_survey_fields=True)
        for record in records:
            self.assertNotIn('redcap_survey_identifier', record)
            self.assertNotIn('demographics_timestamp', record)

    @unittest.skipIf(skip_pd, "Couldn't import pandas")
    @responses.activate
    def test_metadata_to_df(self):
        """Test metadata export --> DataFrame"""
        self.add_normalproject_response()
        df = self.reg_proj.export_metadata(format='df')
        self.assertIsInstance(df, pd.DataFrame)

    @unittest.skipIf(skip_pd, "Couldn't import pandas")
    @responses.activate
    def test_export_to_df(self):
        """Test export --> DataFrame"""
        self.add_normalproject_response()
        self.add_long_project_response()
        df = self.reg_proj.export_records(format='df')
        self.assertIsInstance(df, pd.DataFrame)
        # Test it's a normal index
        self.assertTrue(hasattr(df.index, 'name'))
        # Test for a MultiIndex on longitudinal df
        long_df = self.long_proj.export_records(format='df', event_name='raw')
        self.assertTrue(hasattr(long_df.index, 'names'))

    @unittest.skipIf(skip_pd, "Couldn't import pandas")
    @responses.activate
    def test_export_df_kwargs(self):
        """Test passing kwargs to export DataFrame construction"""
        self.add_normalproject_response()
        df = self.reg_proj.export_records(format='df',
            df_kwargs={'index_col': 'first_name'})
        self.assertEqual(df.index.name, 'first_name')
        self.assertTrue('study_id' in df)

    @unittest.skipIf(skip_pd, "Couldn't import pandas")
    @responses.activate
    def test_metadata_df_kwargs(self):
        """Test passing kwargs to metadata DataFrame construction"""
        self.add_normalproject_response()
        df = self.reg_proj.export_metadata(format='df',
            df_kwargs={'index_col': 'field_label'})
        self.assertEqual(df.index.name, 'field_label')
        self.assertTrue('field_name' in df)

    @unittest.skipIf(skip_pd, "Couldn't import pandas")
    @responses.activate
    def test_import_dataframe(self):
        """Test importing a pandas.DataFrame"""
        self.add_normalproject_response()
        self.add_long_project_response()
        df = self.reg_proj.export_records(format='df')
        response = self.reg_proj.import_records(df)
        self.assertIn('count', response)
        self.assertNotIn('error', response)
        long_df = self.long_proj.export_records(event_name='raw', format='df')
        response = self.long_proj.import_records(long_df)
        self.assertIn('count', response)
        self.assertNotIn('error', response)

    @responses.activate
    def test_date_formatting(self):
        """Test date_format parameter"""
        self.add_normalproject_response()

        def import_factory(date_string):
            return [{'study_id': '1',
                     'dob': date_string}]

        # Default YMD with dashes
        import_ymd = import_factory('2000-01-01')
        response = self.reg_proj.import_records(import_ymd)
        self.assertEqual(response['count'], 1)

        # DMY with /
        import_dmy = import_factory('31/01/2000')
        response = self.reg_proj.import_records(import_dmy, date_format='DMY')
        self.assertEqual(response['count'], 1)

        import_mdy = import_factory('12/31/2000')
        response = self.reg_proj.import_records(import_mdy, date_format='MDY')
        self.assertEqual(response['count'], 1)

    def test_get_version(self):
        """Testing retrieval of REDCap version associated with Project"""
        self.assertTrue(isinstance(semantic_version.Version('1.0.0'), type(self.long_proj.redcap_version)))

    @responses.activate
    def test_export_checkbox_labels(self):
        """Testing the export of checkbox labels as field values"""
        self.add_normalproject_response()
        self.assertEqual(
            self.reg_proj.export_records(
                raw_or_label='label',
                export_checkbox_labels=True)[0]['matcheck1___1'],
                'Foo'
        )

    @responses.activate
    def test_export_always_include_def_field(self):
        """ Ensure def_field always comes in the output even if not explicity
        given in a requested form """
        self.add_normalproject_response()
        # If we just ask for a form, must also get def_field in there
        records = self.reg_proj.export_records(forms=['imaging'])
        for record in records:
            self.assertIn(self.reg_proj.def_field, record)
        # , still need it def_field even if not asked for in form and fields
        records = self.reg_proj.export_records(forms=['imaging'], fields=['foo_score'])
        for record in records:
            self.assertIn(self.reg_proj.def_field, record)
        # If we just ask for some fields, still need def_field
        records = self.reg_proj.export_records(fields=['foo_score'])
        for record in records:
            self.assertIn(self.reg_proj.def_field, record)