From cf01fe2031d0740acbc9d1beb75509100f40630b Mon Sep 17 00:00:00 2001 From: Michael Tran Date: Wed, 3 Feb 2021 21:02:57 -0500 Subject: [PATCH 01/13] ShouldExportRecordAsync => FAIL --- RedcapApi/Broker/ApiBroker.cs | 47 ++ RedcapApi/Broker/IApiBroker.cs | 16 + RedcapApi/Models/Demographic.cs | 29 + RedcapApi/Redcap.csproj | 18 +- RedcapApi/Redcap.xml | 20 + RedcapApi/Services/ApiService.cs | 25 + RedcapApi/Services/IApiService.cs | 9 + RedcapApiDemo/RedcapApiDemo.csproj | 8 +- Tests/RedcapApiTests.DataAccessGroups.cs | 23 + Tests/RedcapApiTests.Records.cs | 41 ++ Tests/RedcapApiTests.cs | 853 +---------------------- Tests/RedcapApiTestsOld.cs | 847 ++++++++++++++++++++++ Tests/Tests.csproj | 24 +- ram_crest_160.png | Bin 12267 -> 0 bytes vcu.png | Bin 0 -> 8467 bytes 15 files changed, 1108 insertions(+), 852 deletions(-) create mode 100644 RedcapApi/Broker/ApiBroker.cs create mode 100644 RedcapApi/Broker/IApiBroker.cs create mode 100644 RedcapApi/Models/Demographic.cs create mode 100644 RedcapApi/Services/ApiService.cs create mode 100644 RedcapApi/Services/IApiService.cs create mode 100644 Tests/RedcapApiTests.DataAccessGroups.cs create mode 100644 Tests/RedcapApiTests.Records.cs create mode 100644 Tests/RedcapApiTestsOld.cs delete mode 100644 ram_crest_160.png create mode 100644 vcu.png diff --git a/RedcapApi/Broker/ApiBroker.cs b/RedcapApi/Broker/ApiBroker.cs new file mode 100644 index 0000000..b5bdc97 --- /dev/null +++ b/RedcapApi/Broker/ApiBroker.cs @@ -0,0 +1,47 @@ +using System; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +using RestSharp; + +using Serilog; + +namespace Redcap.Broker +{ + public partial class ApiBroker: IApiBroker + { + protected readonly HttpClient httpClient; + protected readonly IRestClient restClient; + public ApiBroker(HttpClient httpClient, IRestClient restClient) + { + this.httpClient = httpClient; + this.restClient = restClient; + } + public void LogException(Exception ex, + [CallerMemberName] string method = null, + [CallerFilePath] string filePath = null, + [CallerLineNumber] int lineNumber = 0) + { + var errorMessage = $"Message: {ex.Message}. Method: {method} File: {filePath} LineNumber: {lineNumber}"; + Log.Error($"Message: {ex.Message}. Method: {method} File: {filePath} LineNumber: {lineNumber}"); + throw new Exception(errorMessage); + } + public async Task PostAsync(IRestRequest request, CancellationToken cancellationToken = default) + { + var response = await restClient.PostAsync(request, cancellationToken); + + return response; + } + public async Task ExecuteAsync(RestRequest request) where T : new() + { + var response = await restClient.ExecuteAsync(request); + if(response.ErrorException != null) + { + LogException(response.ErrorException); + } + return response.Data; + } + } +} diff --git a/RedcapApi/Broker/IApiBroker.cs b/RedcapApi/Broker/IApiBroker.cs new file mode 100644 index 0000000..18da857 --- /dev/null +++ b/RedcapApi/Broker/IApiBroker.cs @@ -0,0 +1,16 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +using RestSharp; + +namespace Redcap.Broker +{ + public interface IApiBroker + { + Task ExecuteAsync(RestRequest request) where T : new(); + void LogException(Exception ex, [CallerMemberName] string method = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = 0); + Task PostAsync(IRestRequest request, CancellationToken cancellationToken = default); + } +} diff --git a/RedcapApi/Models/Demographic.cs b/RedcapApi/Models/Demographic.cs new file mode 100644 index 0000000..a432631 --- /dev/null +++ b/RedcapApi/Models/Demographic.cs @@ -0,0 +1,29 @@ +using Newtonsoft.Json; + +namespace Redcap.Models +{ + /// + /// Simplified demographics instrument that we can test with. + /// + public class Demographic + { + /// + /// + /// + [JsonRequired] + [JsonProperty("record_id")] + public string RecordId { get; set; } + + /// + /// + /// + [JsonProperty("first_name")] + public string FirstName { get; set; } + + /// + /// + /// + [JsonProperty("last_name")] + public string LastName { get; set; } + } +} diff --git a/RedcapApi/Redcap.csproj b/RedcapApi/Redcap.csproj index 07b9062..6919266 100644 --- a/RedcapApi/Redcap.csproj +++ b/RedcapApi/Redcap.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + netstandard2.1 Michael Tran Virginia Commonwealth University True @@ -28,7 +28,7 @@ https://github.com/cctrbic/redcap-api/blob/master/LICENSE.md https://vortex.cctr.vcu.edu/images/ram_crest_160.png MIT - ram_crest_160.png + vcu.png false @@ -38,23 +38,25 @@ AnyCPU - D:\Github\redcap-api\RedcapApi\Redcap.xml + - + True - - - + + + + + - + diff --git a/RedcapApi/Redcap.xml b/RedcapApi/Redcap.xml index e24d829..c2cd359 100644 --- a/RedcapApi/Redcap.xml +++ b/RedcapApi/Redcap.xml @@ -3626,6 +3626,26 @@ + + + Simplified demographics instrument that we can test with. + + + + + + + + + + + + + + + + + The format that the response object should be when returned from the http request. diff --git a/RedcapApi/Services/ApiService.cs b/RedcapApi/Services/ApiService.cs new file mode 100644 index 0000000..be3cb17 --- /dev/null +++ b/RedcapApi/Services/ApiService.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading.Tasks; + +using Redcap.Broker; + +namespace Redcap.Services +{ + public class ApiService : IApiService + { + private readonly IApiBroker apiBroker; + public ApiService(IApiBroker apiBroker) + { + this.apiBroker = apiBroker; + } + /// + /// Exports a single record from REDCap. + /// + /// + /// + public async Task ExportRecordAsync() + { + throw new NotImplementedException(); + } + } +} diff --git a/RedcapApi/Services/IApiService.cs b/RedcapApi/Services/IApiService.cs new file mode 100644 index 0000000..c66e6e3 --- /dev/null +++ b/RedcapApi/Services/IApiService.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Redcap.Services +{ + public interface IApiService + { + Task ExportRecordAsync(); + } +} \ No newline at end of file diff --git a/RedcapApiDemo/RedcapApiDemo.csproj b/RedcapApiDemo/RedcapApiDemo.csproj index 2713689..38c7769 100644 --- a/RedcapApiDemo/RedcapApiDemo.csproj +++ b/RedcapApiDemo/RedcapApiDemo.csproj @@ -2,13 +2,13 @@ Exe - netcoreapp2.0 + net5.0 - - - + + + diff --git a/Tests/RedcapApiTests.DataAccessGroups.cs b/Tests/RedcapApiTests.DataAccessGroups.cs new file mode 100644 index 0000000..3b9ea28 --- /dev/null +++ b/Tests/RedcapApiTests.DataAccessGroups.cs @@ -0,0 +1,23 @@ +/* + * REDCap API Library + * Michael Tran tranpl@vcu.edu, tranpl@outlook.com + * Biomedical Informatics Core + * C. Kenneth and Dianne Wright Center for Clinical and Translational Research + * Virginia Commonwealth University + * + * Copyright (c) 2021 Virginia Commonwealth University + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +using Redcap; +using Redcap.Interfaces; + +namespace Tests +{ + public partial class RedcapApiTests + { + } +} diff --git a/Tests/RedcapApiTests.Records.cs b/Tests/RedcapApiTests.Records.cs new file mode 100644 index 0000000..00eec8f --- /dev/null +++ b/Tests/RedcapApiTests.Records.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using FluentAssertions; + +using Moq; + +using Redcap.Broker; +using Redcap.Models; +using Redcap.Services; + +using RestSharp; + +using Xunit; + +namespace Tests +{ + public partial class RedcapApiTests + { + [Fact] + public async Task ShouldExportRecordAsync() + { + // arrange + Demographic demographicInstrument = CreateDemographicsInstrument(); + Demographic inputDemographicInstrument = demographicInstrument; + Demographic retrievedDemographicInstrument = inputDemographicInstrument; + Demographic expectedDemographicInstrument = retrievedDemographicInstrument; + var request = new RestRequest(apiUri, Method.POST); + apiBrokerMock.Setup(broker => broker.ExecuteAsync(request)) + .ReturnsAsync(retrievedDemographicInstrument); + // act + Demographic actualDemographicInstrument = await apiService.ExportRecordAsync(); + + // assert + actualDemographicInstrument.Should().BeEquivalentTo(expectedDemographicInstrument); + } + } +} diff --git a/Tests/RedcapApiTests.cs b/Tests/RedcapApiTests.cs index a954ab7..88c8763 100644 --- a/Tests/RedcapApiTests.cs +++ b/Tests/RedcapApiTests.cs @@ -1,846 +1,37 @@ -using Newtonsoft.Json; -using Redcap; -using Redcap.Models; -using Redcap.Utilities; +using System; using System.Collections.Generic; using System.Linq; -using Xunit; +using System.Text; +using System.Threading.Tasks; -namespace Tests -{ - /// - /// Simplified demographics instrument that we can test with. - /// - public class Demographic - { - [JsonRequired] - [JsonProperty("record_id")] - public string RecordId { get; set; } +using Moq; - [JsonProperty("first_name")] - public string FirstName { get; set; } +using Redcap.Broker; +using Redcap.Models; +using Redcap.Services; - [JsonProperty("last_name")] - public string LastName { get; set; } - } - /// - /// Very simplified test class for Redcap Api - /// This is not a comprehensive test, add more if you'd like. - /// Make sure you have some records in the redcap project for the instance you are testing - /// - public class RedcapApiTests +using Tynamix.ObjectFiller; + +namespace Tests +{ + public partial class RedcapApiTests { - // API Token for a project in a local instance of redcap - private const string _token = "A8E6949EF4380F1111C66D5374E1AE6C"; - // local instance of redcap api uri - private const string _uri = "http://localhost/redcap/api/"; - private readonly RedcapApi _redcapApi; + private readonly IApiService apiService; + private readonly Mock apiBrokerMock; + private string apiUri = "http://localhost:8080/redcap"; public RedcapApiTests() { - _redcapApi = new RedcapApi(_uri); - } - - [Fact] - public async void CanImportRecordsAsyncAsDictionary_ShouldReturn_CountString() - { - // Arrange - var data = new List> { }; - var keyValues = new Dictionary { }; - keyValues.Add("first_name", "Jon"); - keyValues.Add("last_name", "Doe"); - keyValues.Add("record_id", "8"); - keyValues.Add("redcap_repeat_instrument", "demographics"); - data.Add(keyValues); - // Act - var result = await _redcapApi.ImportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, OverwriteBehavior.normal, false, data, "MDY", CsvDelimiter.comma, ReturnContent.count, OnErrorFormat.json); - - // Assert - // Expecting a string of 1 since we are importing one record - Assert.Contains("1", result); - } - [Fact, TestPriority(0)] - public void CanImportRecordsAsync_ShouldReturn_CountString() - { - // Arrange - var data = new List { new Demographic { FirstName = "Jon", LastName = "Doe", RecordId = "1" } }; - // Act - /* - * Using API Version 1.0.0+ - */ - // executing method using default options - var result = _redcapApi.ImportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, OverwriteBehavior.normal, false, data, "MDY", CsvDelimiter.comma, ReturnContent.count, OnErrorFormat.json).Result; - - var res = JsonConvert.DeserializeObject(result).ToString(); - - // Assert - // Expecting a string of 1 since we are importing one record - Assert.Contains("1", res); - } - [Fact] - public async void CanDeleteRecordsAsync_ShouldReturn_string() - { - // Arrange - var record = new List> { }; - var keyValues = new Dictionary { }; - keyValues.Add("first_name", "Jon"); - keyValues.Add("last_name", "Doe"); - keyValues.Add("record_id", "8"); - record.Add(keyValues); - // import records so we can delete it for this test - await _redcapApi.ImportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, OverwriteBehavior.normal, true, record, "MDY", CsvDelimiter.comma, ReturnContent.count, OnErrorFormat.json); - - var records = new string[] - { - "8" - }; - var arm = 1; - // Act - var result = await _redcapApi.DeleteRecordsAsync(_token, Content.Record, RedcapAction.Delete, records, arm); - - // Assert - // Expecting a string of 1 since we are deleting one record - Assert.Contains("1", result); - } - [Fact] - public async void CanImportRepeatingInstrumentsAndEvents_ShouldReturn_string() - { - // exact instrument names in data dictionary to repeat - var repeatingInstruments = new List { - new RedcapRepeatInstrument { - EventName = "event_1_arm_1", - FormName = "demographics", - CustomFormLabel = "TestTestTest" - } - }; - var result = await _redcapApi.ImportRepeatingInstrumentsAndEvents(_token, repeatingInstruments); - // Expect "1" as we are importing a single repeating instrument - Assert.Contains("1", result); - } - /// - /// Test ability to export repeating instruments and events - /// API Version 1.0.0+ - /// - [Fact] - public async void CanExportRepeatingInstrumentsAndEvents_ShouldReturn_string() - { - // Arrange - // We'll importa a single repeating form so we can run test - // By executing the import repeating instrument api, redcap will - // enable the repeating instruments and events feature automatically - var repeatingInstruments = new List { - new RedcapRepeatInstrument { - EventName = "event_1_arm_1", - FormName = "demographics", - CustomFormLabel = "TestTestTest" - } - }; - await _redcapApi.ImportRepeatingInstrumentsAndEvents(_token, repeatingInstruments); - - // Act - /* - * Using API Version 1.0.0+ - */ - var result = await _redcapApi.ExportRepeatingInstrumentsAndEvents(_token); - - // Assert - // Expecting event names, form name and custom form labels - // we imported it above - Assert.Contains("event_name", result); - Assert.Contains("form_name", result); - Assert.Contains("custom_form_label", result); - Assert.Contains("demographics", result); - Assert.Contains("event_1_arm_1", result); - Assert.Contains("TestTestTest", result); - } - - /// - /// Can Export Arms - /// All arms should be returned - /// Using API version 1.0.0+ - /// - [Fact, TestPriority(1)] - public void CanExportArmsAsync_AllArms_ShouldContain_armnum() - { - // Arrange - - - // Act - /* - * Using API Version 1.0.0+ - */ - var result = _redcapApi.ExportArmsAsync(_token).Result; - var data = JsonConvert.DeserializeObject(result).ToString(); - - // Assert - // Expecting multiple arms to be return since we asked for all arms by not providing any arms by passing null for the params - // ** Important to notice is that if we didn't add any events to an arm, even if there are more, only - // arms with events will be returned ** - Assert.Contains("1", data); - Assert.Contains("2", data); - } - - - /// - /// Can Import Arms - /// Using API version 1.0.0+ - /// - [Fact, TestPriority(0)] - public async void CanImportArmsAsync_SingleArm_ShouldReturn_number() - { - // Arrange - var armlist = new List - { - new RedcapArm{ArmNumber = "3", Name = "testarm3_this_will_be_deleted"}, - new RedcapArm{ArmNumber = "2", Name = "testarm2_this_will_be_deleted"}, - new RedcapArm{ArmNumber = "4", Name = "testarm4_this_will_be_deleted"}, - }; - // Act - /* - * Using API Version 1.0.0+ - */ - var result = await _redcapApi.ImportArmsAsync(_token, Content.Arm, Override.False, RedcapAction.Import, ReturnFormat.json, armlist, OnErrorFormat.json); - - // Assert - // Expecting "3", the number of arms imported, since we pass 3 arm to be imported - Assert.Contains("3", result); - } - /// - /// Can Delete Arms - /// Using API version 1.0.0+ - /// - [Fact, TestPriority(99)] - public async void CanDeleteArmsAsync_SingleArm_ShouldReturn_number() - { - // Arrange - // Initially if we enable a project as longitudinal, redcap will append a single arm. - // We are adding a single arm(3) to the project. - var redcapArms = new List { - new RedcapArm { ArmNumber = "3", Name = "Arm 3" }, - }; - - // Make sure we have an arm with a few events before trying to delete one. - var importArmsResults = await _redcapApi.ImportArmsAsync(_token, Content.Arm, Override.True, RedcapAction.Import, ReturnFormat.json, redcapArms, OnErrorFormat.json); - - // arms(#) to be deleted - var armarray = new string[] - { - "3" - }; - - // Act - /* - * Using API Version 1.0.0+ - */ - var result = _redcapApi.DeleteArmsAsync(_token, Content.Arm, RedcapAction.Delete, armarray).Result; - var data = JsonConvert.DeserializeObject(result).ToString(); - - // Assert - // Expecting "1", the number of arms deleted, since we pass 1 arm to be deleted - // You'll need an arm 3 to be available first, run import arm - Assert.Contains("1", data); - } - /// - /// Can Export Events - /// Using API version 1.0.0+ - /// - [Fact] - public void CanExportEventsAsync_SingleEvent_ShouldReturn_event() - { - // Arrange - - var ExportEventsAsyncData = new string[] { "1" }; - - // Act - /* - * Using API Version 1.0.0+ - */ - var result = _redcapApi.ExportEventsAsync(_token, Content.Event, ReturnFormat.json, ExportEventsAsyncData, OnErrorFormat.json).Result; - var data = JsonConvert.DeserializeObject(result).ToString(); - - // Assert - Assert.Contains("event_name", data); - } - /// - /// Can Import Events - /// Using API version 1.0.0+ - /// - [Fact, TestPriority(0)] - public void CanImportEventsAsync_MultipleEvents_ShouldReturn_number() - { - // Arrange - - var apiEndpoint = _uri; - var eventList = new List { - new RedcapEvent { - EventName = "Event 1", - ArmNumber = "1", - DayOffset = "1", - MinimumOffset = "0", - MaximumOffset = "0", - UniqueEventName = "event_1_arm_1", - CustomEventLabel = "Baseline" - }, - new RedcapEvent { - EventName = "Event 2", - ArmNumber = "1", - DayOffset = "1", - MinimumOffset = "0", - MaximumOffset = "0", - UniqueEventName = "event_2_arm_1", - CustomEventLabel = "First Visit" - }, - new RedcapEvent { - EventName = "Event 3", - ArmNumber = "1", - DayOffset = "1", - MinimumOffset = "0", - MaximumOffset = "0", - UniqueEventName = "event_3_arm_1", - CustomEventLabel = "Clinical" - } - }; - - // Act - /* - * Using API Version 1.0.0+ - */ - var _redcapApi = new RedcapApi(apiEndpoint); - var result = _redcapApi.ImportEventsAsync(_token, Content.Event, RedcapAction.Import, Override.False, ReturnFormat.json, eventList, OnErrorFormat.json).Result; - var data = JsonConvert.DeserializeObject(result).ToString(); - - // Assert - // Expecting "3", since we had 3 redcap events imported - Assert.Contains("3", data); - } - /// - /// Can delete Events - /// Using API version 1.0.0+ - /// - [Fact, TestPriority(10)] - public async void CanDeleteEventsAsync_SingleEvent_ShouldReturn_number() - { - // Arrange - var events = new List { - new RedcapEvent { - ArmNumber = "1", - EventName = "Clinical Event 1", - UniqueEventName = "clinical_arm_1"} - }; - await _redcapApi.ImportEventsAsync(_token, Content.Event, RedcapAction.Import, Override.True, ReturnFormat.json, events); - // the event above, redcap appends _arm_1 when you add an arm_# - var DeleteEventsAsyncData = new string[] { "clinical_event_1_arm_1" }; - - // Act - /* - * Using API Version 1.0.0+ - */ - var result = await _redcapApi.DeleteEventsAsync(_token, Content.Event, RedcapAction.Delete, DeleteEventsAsyncData); - - // Assert - // Expecting "1", since we had 1 redcap events imported - Assert.Contains("1", result); - } - [Fact] - public void CanExportRecordAsync_SingleRecord_ShouldReturn_String_1() - { - // Arrange - var recordId = "1"; - // Act - /* - * Just passing the required parameters - */ - var result = _redcapApi.ExportRecordAsync(_token, Content.Record, recordId).Result; - - // Assert - Assert.Contains("1", result); - - } - /// - /// Export / Get single record - /// - [Fact] - public async void CanExportRecordsAsync_SingleRecord_FromRepeatingInstrument_ShouldContain_string_1() - { - // Arrange - var record = new string[] - { - "1" - }; - var redcapEvent = new string[] { "event_1_arm_1" }; - // Act - var result = await _redcapApi.ExportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, record, null, null, redcapEvent); - - // Assert - Assert.Contains("1", result); - } - /// - /// Can export multiple records - /// - [Fact] - public async void CanExportRecordsAsync_MultipleRecord_ShouldContain_string_1_2() - { - // Arrange - var records = new string[] { "1, 2" }; - - // Act - var result = await _redcapApi.ExportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, records); - - // Assert - Assert.Contains("1", result); - Assert.Contains("2", result); - } - /// - /// Can export single record - /// - [Fact] - public async void CanExportRecordAsync_SingleRecord_ShouldContain_string_1() - { - // Arrange - var records = new string[] { "1" }; - - // Act - var result = await _redcapApi.ExportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, records); - - // Assert - Assert.Contains("1", result); - } - - /// - /// Can export redcap version - /// API Version 1.0.0+ - /// - [Fact] - public async void CanExportRedcapVersion_VersionNumber_Shouldontain_Number() - { - // Arrange - // Assume current redcap version is 8+ - var currentRedcapVersion = "8"; - - // Act - var result = await _redcapApi.ExportRedcapVersionAsync(_token, Content.Version); - - // Assert - // Any version 8.XX will do - Assert.Contains(currentRedcapVersion, result); - - } - /// - /// Can export users - /// API Version 1.0.0+ - /// - [Fact] - public async void CanExportUsers_AllUsers_ShouldReturn_username() - { - // Arrange - var username = "tranpl"; - - // Act - var result = await _redcapApi.ExportUsersAsync(_token, ReturnFormat.json); - // Assert - Assert.Contains(username, result); - - } - - /// - /// Can export records - /// - [Fact] - public async void CanExportRecordsAsync_AllRecords_ShouldReturn_String() - { - // Arrange - - // Act - var result = await _redcapApi.ExportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat); - var data = JsonConvert.DeserializeObject>(result); - - // Assert - // expecting a list of - Assert.True(data.Count > 1); - } - /// - /// Can import meta data - /// This method allows you to import metadata (i.e., Data Dictionary) into a project. - /// Notice: Because of this method's destructive nature, it is only available for use for projects in Development status. - /// API Version 1.0.0+ - /// - [Fact] - public async void CanImportMetaDataAsync_Metadata_ShouldReturn_string_record_id() - { - // Arrange - // This will wipe out the current data dictionary and update with the below meta. - var metata = new List { - new RedcapMetaData{ - field_name ="record_id", - form_name = "demographics", - field_label ="Study Id", - field_type ="text", - section_header = "", - }, - new RedcapMetaData{ - field_name ="first_name", - form_name = "demographics", - field_label ="First Name", - field_type ="text", - section_header = "Contact Information", - identifier = "y" - }, - new RedcapMetaData{ - field_name ="last_name", - form_name = "demographics", - field_label ="Last Name", - field_type ="text", - identifier = "y" - }, - new RedcapMetaData{ - field_name ="address", - form_name = "demographics", - field_label ="Street, City, State, ZIP", - field_type ="notes", - identifier = "y" - }, - new RedcapMetaData{ - field_name ="email", - form_name = "demographics", - field_label ="E-mail", - field_type ="text", - identifier = "y", - text_validation_type_or_show_slider_number = "email" - }, - new RedcapMetaData{ - field_name ="dob", - form_name = "demographics", - field_label ="Date of Birth", - field_type ="text", - identifier = "y", - text_validation_type_or_show_slider_number = "date_ymd" - }, - new RedcapMetaData{ - field_name ="file_upload", - form_name = "demographics", - field_label ="File Upload", - field_type ="file" - } - }; - // Act - var result = await _redcapApi.ImportMetaDataAsync(_token, Content.MetaData, ReturnFormat.json, metata); - - // Assert - // Expecting 7 metada objects imported - Assert.Contains("7", result); + apiBrokerMock = new Mock(); + apiService = new ApiService(apiBroker: apiBrokerMock.Object); } - /// - /// Can export meta data - /// - [Fact] - public async void CanExportMetaDataAsync_Metadata_ShouldReturn_NumberMetadataFields() + private static Demographic CreateDemographicsInstrument() { - // This will wipe out the current data dictionary and update with the below meta. - var metata = new List { - new RedcapMetaData{ - field_name ="record_id", - form_name = "demographics", - field_label ="Study Id", - field_type ="text", - section_header = "", - }, - new RedcapMetaData{ - field_name ="first_name", - form_name = "demographics", - field_label ="First Name", - field_type ="text", - section_header = "Contact Information", - identifier = "y" - }, - new RedcapMetaData{ - field_name ="last_name", - form_name = "demographics", - field_label ="Last Name", - field_type ="text", - identifier = "y" - }, - new RedcapMetaData{ - field_name ="address", - form_name = "demographics", - field_label ="Street, City, State, ZIP", - field_type ="notes", - identifier = "y" - }, - new RedcapMetaData{ - field_name ="email", - form_name = "demographics", - field_label ="E-mail", - field_type ="text", - identifier = "y", - text_validation_type_or_show_slider_number = "email" - }, - new RedcapMetaData{ - field_name ="dob", - form_name = "demographics", - field_label ="Date of Birth", - field_type ="text", - identifier = "y", - text_validation_type_or_show_slider_number = "date_ymd" - }, - new RedcapMetaData{ - field_name ="file_upload", - form_name = "demographics", - field_label ="File Upload", - field_type ="file" - } - }; - // import 7 metadata fields into the project - // this creates an instrument named demographics with 7 fields - await _redcapApi.ImportMetaDataAsync(_token, Content.MetaData, ReturnFormat.json, metata); - - // Act - var result = await _redcapApi.ExportMetaDataAsync(_token, ReturnFormat.json); - var data = JsonConvert.DeserializeObject>(result); - // Assert - // expecting 7 metadata to be exported - Assert.True(data.Count >= 7); - } - /// - /// Can export arms - /// - [Fact] - public async void CanExportArmsAsync_Arms_ShouldReturn_arms_array() - { - // Arrange - // Importing 3 arms so that we can run the test to export - var redcapArms = new List - { - new RedcapArm{ArmNumber = "3", Name = "testarm3_this_will_be_deleted"}, - new RedcapArm{ArmNumber = "2", Name = "testarm2_this_will_be_deleted"}, - new RedcapArm{ArmNumber = "4", Name = "testarm4_this_will_be_deleted"}, - }; - // Import Arms so we can test - await _redcapApi.ImportArmsAsync(_token, Content.Arm, Override.False, RedcapAction.Import, ReturnFormat.json, redcapArms, OnErrorFormat.json); - var listOfEvents = new List() { - new RedcapEvent{ - ArmNumber = "2", - CustomEventLabel = "HelloEvent1", - EventName = "Import Event 1", - DayOffset = "1", - MinimumOffset = "0", - MaximumOffset = "0", - UniqueEventName = "import_event_1_arm_2" - }, - new RedcapEvent{ - ArmNumber = "2", - CustomEventLabel = "HelloEvent2", - EventName = "Import Event 2", - DayOffset = "1", - MinimumOffset = "0", - MaximumOffset = "0", - UniqueEventName = "import_event_2_arm_2" - }, - new RedcapEvent{ - ArmNumber = "2", - CustomEventLabel = "HelloEvent3", - EventName = "Import Event 3", - DayOffset = "1", - MinimumOffset = "0", - MaximumOffset = "0", - UniqueEventName = "import_event_3_arm_2" - } - }; - // Import Events so we can test - await _redcapApi.ImportEventsAsync(_token, Override.False, ReturnFormat.json, listOfEvents, OnErrorFormat.json); - // we want to export arm 1 and arm 2 - var exportArms = new string[] { "1", "2" }; - // Act - var result = await _redcapApi.ExportArmsAsync(_token, Content.Arm, ReturnFormat.json, exportArms); - - // Assert - // In order for the arms array to be returned, events for the specific arm - // needs to be present. An arm without any events will not be returned. - Assert.Contains("arm_num", result); - Assert.Contains("1", result); - Assert.Contains("2", result); - } - /// - /// Can import arms - /// - [Fact, TestPriority(0)] - public async void CanImportEventsAsync_Events_ShouldReturn_Number() - { - // Arrange - var listOfEvents = new List() { - new RedcapEvent{ - ArmNumber = "3", - CustomEventLabel = "HelloEvent1", - EventName = "Import Event 1", - DayOffset = "1", - MinimumOffset = "0", - MaximumOffset = "0", - UniqueEventName = "import_event_1_arm_3" - }, - new RedcapEvent{ - ArmNumber = "3", - CustomEventLabel = "HelloEvent2", - EventName = "Import Event 2", - DayOffset = "1", - MinimumOffset = "0", - MaximumOffset = "0", - UniqueEventName = "import_event_2_arm_3" - }, - new RedcapEvent{ - ArmNumber = "3", - CustomEventLabel = "HelloEvent3", - EventName = "Import Event 3", - DayOffset = "1", - MinimumOffset = "0", - MaximumOffset = "0", - UniqueEventName = "import_event_3_arm_3" - } - }; - - // Act - var result = await _redcapApi.ImportEventsAsync(_token, Override.False, ReturnFormat.json, listOfEvents, OnErrorFormat.json); - - // Assert - // Expecting 3 since we imported 3 events - Assert.Contains("3", result); - } - /// - /// Test attempts to import a file into the redcap project - /// There are a few assumptions, please make sure you have the files and folders - /// exactly as shown, or name it to your needs. - /// API Version 1.0.0+ - /// - [Fact, TestPriority(0)] - public async void CanImportFileAsync_File_ShouldReturn_Empty_string() - { - // Arrange - - var pathImport = "C:\\redcap_download_files"; - string importFileName = "test.txt"; - var record = "1"; - var fieldName = "file_upload"; - var eventName = "event_1_arm_1"; - var repeatingInstrument = "1"; - - // Act - var result = await _redcapApi.ImportFileAsync(_token, Content.File, RedcapAction.Import, record, fieldName, eventName, repeatingInstrument, importFileName, pathImport, OnErrorFormat.json); - - // Assert - // Expecting an empty string. This API returns an empty string on success. - Assert.True(string.IsNullOrEmpty(result)); - } - /// - /// Test attempts exports the file previously imported - /// - [Fact] - public async void CanExportFileAsync_File_ShouldReturn_string() - { - // Arrange - var pathExport = "C:\\redcap_download_files"; - var record = "1"; - var fieldName = "file_upload"; - var eventName = "event_1_arm_1"; - var repeatingInstrument = "1"; - var expectedString = "test.txt"; - var pathImport = "C:\\redcap_download_files"; - string importFileName = "test.txt"; - // we need to import a file first so we can test the export api - await _redcapApi.ImportFileAsync(_token, Content.File, RedcapAction.Import, record, fieldName, eventName, repeatingInstrument, importFileName, pathImport, OnErrorFormat.json); - - // Act - var result = await _redcapApi.ExportFileAsync(_token, Content.File, RedcapAction.Export, record, fieldName, eventName, repeatingInstrument, OnErrorFormat.json, pathExport); - - // Assert - Assert.Contains(expectedString, result); - } - /// - /// Can delete file previously uploaded - /// - [Fact] - public async void CanDeleteFileAsync_File_ShouldReturn_Empty_string() - { - // Arrange - - var pathImport = "C:\\redcap_download_files"; - string importFileName = "test.txt"; - var record = "1"; - var fieldName = "protocol_upload"; - // In order for us to import a file in a longitudinal format, we'll need to specify - // which event it belongs. - var eventName = "event_1_arm_1"; - var repeatingInstrument = "1"; - // Import a file to a record so we can do the integrated test below. - await _redcapApi.ImportFileAsync(_token, Content.File, RedcapAction.Import, record, fieldName, eventName, repeatingInstrument, importFileName, pathImport, OnErrorFormat.json); - - // Act - var result = await _redcapApi.DeleteFileAsync(_token, Content.File, RedcapAction.Delete, record, fieldName, eventName, repeatingInstrument, OnErrorFormat.json); - - // Assert - Assert.Contains(string.Empty, result); + return CreateDemographicsInstrumentFiller().Create(); } - /// - /// Can export records for projects that does not contain repeating forms/instruments - /// API Version 1.0.0+ - /// - [Fact] - public async void CanExportRecordsAsync_NonRepeating_Should_Return_String() + private static Filler CreateDemographicsInstrumentFiller() { - // Arrange - // Arrange - var data = new List> { }; - var keyValues = new Dictionary { }; - keyValues.Add("first_name", "Jon"); - keyValues.Add("last_name", "Doe"); - keyValues.Add("record_id", "8"); - data.Add(keyValues); - // import a record so we can test - await _redcapApi.ImportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, OverwriteBehavior.normal, false, data, "MDY", CsvDelimiter.comma, ReturnContent.count, OnErrorFormat.json); - - var records = new string[] { "8" }; - - // Act - var result = await _redcapApi.ExportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, records); - - // Assert - Assert.Contains("1", result); - } - /// - /// Can export records for project with repeating instruments/forms - /// API Version 1.0.0+ - /// - [Fact] - public async void CanExportRecordsAsync_Repeating_Should_Return_Record() - { - // Arrange - var repeatingInstruments = new List { - new RedcapRepeatInstrument { - EventName = "event_1_arm_1", - FormName = "demographics", - CustomFormLabel = "TestTestTest" - } - }; - - // We import a repeating instrument to 'turn on' repeating instruments feature - // as well as setting an initial repeating instrument - await _redcapApi.ImportRepeatingInstrumentsAndEvents(_token, repeatingInstruments); - // Get the redcap event from above - var redcapEvent = repeatingInstruments.FirstOrDefault(); - var data = new List> { }; - // passing in minimum requirements for a project with repeating instruments/forms - var keyValues = new Dictionary { }; - keyValues.Add("first_name", "Jon"); - keyValues.Add("last_name", "Doe"); - keyValues.Add("record_id", "8"); - keyValues.Add("redcap_repeat_instance", "1"); - keyValues.Add("redcap_repeat_instrument", redcapEvent.FormName); - data.Add(keyValues); - // import a record so we can test - await _redcapApi.ImportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, OverwriteBehavior.normal, false, data, "MDY", CsvDelimiter.comma, ReturnContent.count, OnErrorFormat.json); - var records = new string[] { "8" }; - - // Act - var result = await _redcapApi.ExportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, records); - - // Assert - Assert.Contains("1", result); + var filler = new Filler(); + return filler; } } } diff --git a/Tests/RedcapApiTestsOld.cs b/Tests/RedcapApiTestsOld.cs new file mode 100644 index 0000000..0272e55 --- /dev/null +++ b/Tests/RedcapApiTestsOld.cs @@ -0,0 +1,847 @@ +/* + * REDCap API Library + * Michael Tran tranpl@vcu.edu, tranpl@outlook.com + * Biomedical Informatics Core + * C. Kenneth and Dianne Wright Center for Clinical and Translational Research + * Virginia Commonwealth University + * + * Copyright (c) 2021 Virginia Commonwealth University + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +using System.Collections.Generic; +using System.Linq; + +using Newtonsoft.Json; + +using Redcap; +using Redcap.Models; +using Redcap.Utilities; + +using Xunit; + +namespace Tests +{ + /// + /// Very simplified test class for Redcap Api + /// This is not a comprehensive test, add more if you'd like. + /// Make sure you have some records in the redcap project for the instance you are testing + /// + public class RedcapApiTestsOld + { + // API Token for a project in a local instance of redcap + private const string _token = "A8E6949EF4380F1111C66D5374E1AE6C"; + // local instance of redcap api uri + private const string _uri = "http://localhost/redcap/api/"; + private readonly RedcapApi _redcapApi; + public RedcapApiTestsOld() + { + _redcapApi = new RedcapApi(_uri); + } + + [Fact] + public async void CanImportRecordsAsyncAsDictionary_ShouldReturn_CountString() + { + // Arrange + var data = new List> { }; + var keyValues = new Dictionary { }; + keyValues.Add("first_name", "Jon"); + keyValues.Add("last_name", "Doe"); + keyValues.Add("record_id", "8"); + keyValues.Add("redcap_repeat_instrument", "demographics"); + data.Add(keyValues); + // Act + var result = await _redcapApi.ImportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, OverwriteBehavior.normal, false, data, "MDY", CsvDelimiter.comma, ReturnContent.count, OnErrorFormat.json); + + // Assert + // Expecting a string of 1 since we are importing one record + Assert.Contains("1", result); + } + [Fact, TestPriority(0)] + public void CanImportRecordsAsync_ShouldReturn_CountString() + { + // Arrange + var data = new List { new Demographic { FirstName = "Jon", LastName = "Doe", RecordId = "1" } }; + // Act + /* + * Using API Version 1.0.0+ + */ + // executing method using default options + var result = _redcapApi.ImportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, OverwriteBehavior.normal, false, data, "MDY", CsvDelimiter.comma, ReturnContent.count, OnErrorFormat.json).Result; + + var res = JsonConvert.DeserializeObject(result).ToString(); + + // Assert + // Expecting a string of 1 since we are importing one record + Assert.Contains("1", res); + } + [Fact] + public async void CanDeleteRecordsAsync_ShouldReturn_string() + { + // Arrange + var record = new List> { }; + var keyValues = new Dictionary { }; + keyValues.Add("first_name", "Jon"); + keyValues.Add("last_name", "Doe"); + keyValues.Add("record_id", "8"); + record.Add(keyValues); + // import records so we can delete it for this test + await _redcapApi.ImportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, OverwriteBehavior.normal, true, record, "MDY", CsvDelimiter.comma, ReturnContent.count, OnErrorFormat.json); + + var records = new string[] + { + "8" + }; + var arm = 1; + // Act + var result = await _redcapApi.DeleteRecordsAsync(_token, Content.Record, RedcapAction.Delete, records, arm); + + // Assert + // Expecting a string of 1 since we are deleting one record + Assert.Contains("1", result); + } + [Fact] + public async void CanImportRepeatingInstrumentsAndEvents_ShouldReturn_string() + { + // exact instrument names in data dictionary to repeat + var repeatingInstruments = new List { + new RedcapRepeatInstrument { + EventName = "event_1_arm_1", + FormName = "demographics", + CustomFormLabel = "TestTestTest" + } + }; + var result = await _redcapApi.ImportRepeatingInstrumentsAndEvents(_token, repeatingInstruments); + // Expect "1" as we are importing a single repeating instrument + Assert.Contains("1", result); + } + /// + /// Test ability to export repeating instruments and events + /// API Version 1.0.0+ + /// + [Fact] + public async void CanExportRepeatingInstrumentsAndEvents_ShouldReturn_string() + { + // Arrange + // We'll importa a single repeating form so we can run test + // By executing the import repeating instrument api, redcap will + // enable the repeating instruments and events feature automatically + var repeatingInstruments = new List { + new RedcapRepeatInstrument { + EventName = "event_1_arm_1", + FormName = "demographics", + CustomFormLabel = "TestTestTest" + } + }; + await _redcapApi.ImportRepeatingInstrumentsAndEvents(_token, repeatingInstruments); + + // Act + /* + * Using API Version 1.0.0+ + */ + var result = await _redcapApi.ExportRepeatingInstrumentsAndEvents(_token); + + // Assert + // Expecting event names, form name and custom form labels + // we imported it above + Assert.Contains("event_name", result); + Assert.Contains("form_name", result); + Assert.Contains("custom_form_label", result); + Assert.Contains("demographics", result); + Assert.Contains("event_1_arm_1", result); + Assert.Contains("TestTestTest", result); + } + + /// + /// Can Export Arms + /// All arms should be returned + /// Using API version 1.0.0+ + /// + [Fact, TestPriority(1)] + public void CanExportArmsAsync_AllArms_ShouldContain_armnum() + { + // Arrange + + + // Act + /* + * Using API Version 1.0.0+ + */ + var result = _redcapApi.ExportArmsAsync(_token).Result; + var data = JsonConvert.DeserializeObject(result).ToString(); + + // Assert + // Expecting multiple arms to be return since we asked for all arms by not providing any arms by passing null for the params + // ** Important to notice is that if we didn't add any events to an arm, even if there are more, only + // arms with events will be returned ** + Assert.Contains("1", data); + Assert.Contains("2", data); + } + + + /// + /// Can Import Arms + /// Using API version 1.0.0+ + /// + [Fact, TestPriority(0)] + public async void CanImportArmsAsync_SingleArm_ShouldReturn_number() + { + // Arrange + var armlist = new List + { + new RedcapArm{ArmNumber = "3", Name = "testarm3_this_will_be_deleted"}, + new RedcapArm{ArmNumber = "2", Name = "testarm2_this_will_be_deleted"}, + new RedcapArm{ArmNumber = "4", Name = "testarm4_this_will_be_deleted"}, + }; + // Act + /* + * Using API Version 1.0.0+ + */ + var result = await _redcapApi.ImportArmsAsync(_token, Content.Arm, Override.False, RedcapAction.Import, ReturnFormat.json, armlist, OnErrorFormat.json); + + // Assert + // Expecting "3", the number of arms imported, since we pass 3 arm to be imported + Assert.Contains("3", result); + } + /// + /// Can Delete Arms + /// Using API version 1.0.0+ + /// + [Fact, TestPriority(99)] + public async void CanDeleteArmsAsync_SingleArm_ShouldReturn_number() + { + // Arrange + // Initially if we enable a project as longitudinal, redcap will append a single arm. + // We are adding a single arm(3) to the project. + var redcapArms = new List { + new RedcapArm { ArmNumber = "3", Name = "Arm 3" }, + }; + + // Make sure we have an arm with a few events before trying to delete one. + var importArmsResults = await _redcapApi.ImportArmsAsync(_token, Content.Arm, Override.True, RedcapAction.Import, ReturnFormat.json, redcapArms, OnErrorFormat.json); + + // arms(#) to be deleted + var armarray = new string[] + { + "3" + }; + + // Act + /* + * Using API Version 1.0.0+ + */ + var result = _redcapApi.DeleteArmsAsync(_token, Content.Arm, RedcapAction.Delete, armarray).Result; + var data = JsonConvert.DeserializeObject(result).ToString(); + + // Assert + // Expecting "1", the number of arms deleted, since we pass 1 arm to be deleted + // You'll need an arm 3 to be available first, run import arm + Assert.Contains("1", data); + } + /// + /// Can Export Events + /// Using API version 1.0.0+ + /// + [Fact] + public void CanExportEventsAsync_SingleEvent_ShouldReturn_event() + { + // Arrange + + var ExportEventsAsyncData = new string[] { "1" }; + + // Act + /* + * Using API Version 1.0.0+ + */ + var result = _redcapApi.ExportEventsAsync(_token, Content.Event, ReturnFormat.json, ExportEventsAsyncData, OnErrorFormat.json).Result; + var data = JsonConvert.DeserializeObject(result).ToString(); + + // Assert + Assert.Contains("event_name", data); + } + /// + /// Can Import Events + /// Using API version 1.0.0+ + /// + [Fact, TestPriority(0)] + public void CanImportEventsAsync_MultipleEvents_ShouldReturn_number() + { + // Arrange + + var apiEndpoint = _uri; + var eventList = new List { + new RedcapEvent { + EventName = "Event 1", + ArmNumber = "1", + DayOffset = "1", + MinimumOffset = "0", + MaximumOffset = "0", + UniqueEventName = "event_1_arm_1", + CustomEventLabel = "Baseline" + }, + new RedcapEvent { + EventName = "Event 2", + ArmNumber = "1", + DayOffset = "1", + MinimumOffset = "0", + MaximumOffset = "0", + UniqueEventName = "event_2_arm_1", + CustomEventLabel = "First Visit" + }, + new RedcapEvent { + EventName = "Event 3", + ArmNumber = "1", + DayOffset = "1", + MinimumOffset = "0", + MaximumOffset = "0", + UniqueEventName = "event_3_arm_1", + CustomEventLabel = "Clinical" + } + }; + + // Act + /* + * Using API Version 1.0.0+ + */ + var _redcapApi = new RedcapApi(apiEndpoint); + var result = _redcapApi.ImportEventsAsync(_token, Content.Event, RedcapAction.Import, Override.False, ReturnFormat.json, eventList, OnErrorFormat.json).Result; + var data = JsonConvert.DeserializeObject(result).ToString(); + + // Assert + // Expecting "3", since we had 3 redcap events imported + Assert.Contains("3", data); + } + /// + /// Can delete Events + /// Using API version 1.0.0+ + /// + [Fact, TestPriority(10)] + public async void CanDeleteEventsAsync_SingleEvent_ShouldReturn_number() + { + // Arrange + var events = new List { + new RedcapEvent { + ArmNumber = "1", + EventName = "Clinical Event 1", + UniqueEventName = "clinical_arm_1"} + }; + await _redcapApi.ImportEventsAsync(_token, Content.Event, RedcapAction.Import, Override.True, ReturnFormat.json, events); + // the event above, redcap appends _arm_1 when you add an arm_# + var DeleteEventsAsyncData = new string[] { "clinical_event_1_arm_1" }; + + // Act + /* + * Using API Version 1.0.0+ + */ + var result = await _redcapApi.DeleteEventsAsync(_token, Content.Event, RedcapAction.Delete, DeleteEventsAsyncData); + + // Assert + // Expecting "1", since we had 1 redcap events imported + Assert.Contains("1", result); + } + [Fact] + public void CanExportRecordAsync_SingleRecord_ShouldReturn_String_1() + { + // Arrange + var recordId = "1"; + // Act + /* + * Just passing the required parameters + */ + var result = _redcapApi.ExportRecordAsync(_token, Content.Record, recordId).Result; + + // Assert + Assert.Contains("1", result); + + } + /// + /// Export / Get single record + /// + [Fact] + public async void CanExportRecordsAsync_SingleRecord_FromRepeatingInstrument_ShouldContain_string_1() + { + // Arrange + var record = new string[] + { + "1" + }; + var redcapEvent = new string[] { "event_1_arm_1" }; + // Act + var result = await _redcapApi.ExportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, record, null, null, redcapEvent); + + // Assert + Assert.Contains("1", result); + } + /// + /// Can export multiple records + /// + [Fact] + public async void CanExportRecordsAsync_MultipleRecord_ShouldContain_string_1_2() + { + // Arrange + var records = new string[] { "1, 2" }; + + // Act + var result = await _redcapApi.ExportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, records); + + // Assert + Assert.Contains("1", result); + Assert.Contains("2", result); + } + /// + /// Can export single record + /// + [Fact] + public async void CanExportRecordAsync_SingleRecord_ShouldContain_string_1() + { + // Arrange + var records = new string[] { "1" }; + + // Act + var result = await _redcapApi.ExportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, records); + + // Assert + Assert.Contains("1", result); + } + + /// + /// Can export redcap version + /// API Version 1.0.0+ + /// + [Fact] + public async void CanExportRedcapVersion_VersionNumber_Shouldontain_Number() + { + // Arrange + // Assume current redcap version is 8+ + var currentRedcapVersion = "8"; + + // Act + var result = await _redcapApi.ExportRedcapVersionAsync(_token, Content.Version); + + // Assert + // Any version 8.XX will do + Assert.Contains(currentRedcapVersion, result); + + } + /// + /// Can export users + /// API Version 1.0.0+ + /// + [Fact] + public async void CanExportUsers_AllUsers_ShouldReturn_username() + { + // Arrange + var username = "tranpl"; + + // Act + var result = await _redcapApi.ExportUsersAsync(_token, ReturnFormat.json); + // Assert + Assert.Contains(username, result); + + } + + /// + /// Can export records + /// + [Fact] + public async void CanExportRecordsAsync_AllRecords_ShouldReturn_String() + { + // Arrange + + // Act + var result = await _redcapApi.ExportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat); + var data = JsonConvert.DeserializeObject>(result); + + // Assert + // expecting a list of + Assert.True(data.Count > 1); + } + /// + /// Can import meta data + /// This method allows you to import metadata (i.e., Data Dictionary) into a project. + /// Notice: Because of this method's destructive nature, it is only available for use for projects in Development status. + /// API Version 1.0.0+ + /// + [Fact] + public async void CanImportMetaDataAsync_Metadata_ShouldReturn_string_record_id() + { + // Arrange + // This will wipe out the current data dictionary and update with the below meta. + var metata = new List { + new RedcapMetaData{ + field_name ="record_id", + form_name = "demographics", + field_label ="Study Id", + field_type ="text", + section_header = "", + }, + new RedcapMetaData{ + field_name ="first_name", + form_name = "demographics", + field_label ="First Name", + field_type ="text", + section_header = "Contact Information", + identifier = "y" + }, + new RedcapMetaData{ + field_name ="last_name", + form_name = "demographics", + field_label ="Last Name", + field_type ="text", + identifier = "y" + }, + new RedcapMetaData{ + field_name ="address", + form_name = "demographics", + field_label ="Street, City, State, ZIP", + field_type ="notes", + identifier = "y" + }, + new RedcapMetaData{ + field_name ="email", + form_name = "demographics", + field_label ="E-mail", + field_type ="text", + identifier = "y", + text_validation_type_or_show_slider_number = "email" + }, + new RedcapMetaData{ + field_name ="dob", + form_name = "demographics", + field_label ="Date of Birth", + field_type ="text", + identifier = "y", + text_validation_type_or_show_slider_number = "date_ymd" + }, + new RedcapMetaData{ + field_name ="file_upload", + form_name = "demographics", + field_label ="File Upload", + field_type ="file" + } + }; + // Act + var result = await _redcapApi.ImportMetaDataAsync(_token, Content.MetaData, ReturnFormat.json, metata); + + // Assert + // Expecting 7 metada objects imported + Assert.Contains("7", result); + } + /// + /// Can export meta data + /// + [Fact] + public async void CanExportMetaDataAsync_Metadata_ShouldReturn_NumberMetadataFields() + { + // This will wipe out the current data dictionary and update with the below meta. + var metata = new List { + new RedcapMetaData{ + field_name ="record_id", + form_name = "demographics", + field_label ="Study Id", + field_type ="text", + section_header = "", + }, + new RedcapMetaData{ + field_name ="first_name", + form_name = "demographics", + field_label ="First Name", + field_type ="text", + section_header = "Contact Information", + identifier = "y" + }, + new RedcapMetaData{ + field_name ="last_name", + form_name = "demographics", + field_label ="Last Name", + field_type ="text", + identifier = "y" + }, + new RedcapMetaData{ + field_name ="address", + form_name = "demographics", + field_label ="Street, City, State, ZIP", + field_type ="notes", + identifier = "y" + }, + new RedcapMetaData{ + field_name ="email", + form_name = "demographics", + field_label ="E-mail", + field_type ="text", + identifier = "y", + text_validation_type_or_show_slider_number = "email" + }, + new RedcapMetaData{ + field_name ="dob", + form_name = "demographics", + field_label ="Date of Birth", + field_type ="text", + identifier = "y", + text_validation_type_or_show_slider_number = "date_ymd" + }, + new RedcapMetaData{ + field_name ="file_upload", + form_name = "demographics", + field_label ="File Upload", + field_type ="file" + } + }; + // import 7 metadata fields into the project + // this creates an instrument named demographics with 7 fields + await _redcapApi.ImportMetaDataAsync(_token, Content.MetaData, ReturnFormat.json, metata); + + // Act + var result = await _redcapApi.ExportMetaDataAsync(_token, ReturnFormat.json); + var data = JsonConvert.DeserializeObject>(result); + // Assert + // expecting 7 metadata to be exported + Assert.True(data.Count >= 7); + } + /// + /// Can export arms + /// + [Fact] + public async void CanExportArmsAsync_Arms_ShouldReturn_arms_array() + { + // Arrange + // Importing 3 arms so that we can run the test to export + var redcapArms = new List + { + new RedcapArm{ArmNumber = "3", Name = "testarm3_this_will_be_deleted"}, + new RedcapArm{ArmNumber = "2", Name = "testarm2_this_will_be_deleted"}, + new RedcapArm{ArmNumber = "4", Name = "testarm4_this_will_be_deleted"}, + }; + // Import Arms so we can test + await _redcapApi.ImportArmsAsync(_token, Content.Arm, Override.False, RedcapAction.Import, ReturnFormat.json, redcapArms, OnErrorFormat.json); + var listOfEvents = new List() { + new RedcapEvent{ + ArmNumber = "2", + CustomEventLabel = "HelloEvent1", + EventName = "Import Event 1", + DayOffset = "1", + MinimumOffset = "0", + MaximumOffset = "0", + UniqueEventName = "import_event_1_arm_2" + }, + new RedcapEvent{ + ArmNumber = "2", + CustomEventLabel = "HelloEvent2", + EventName = "Import Event 2", + DayOffset = "1", + MinimumOffset = "0", + MaximumOffset = "0", + UniqueEventName = "import_event_2_arm_2" + }, + new RedcapEvent{ + ArmNumber = "2", + CustomEventLabel = "HelloEvent3", + EventName = "Import Event 3", + DayOffset = "1", + MinimumOffset = "0", + MaximumOffset = "0", + UniqueEventName = "import_event_3_arm_2" + } + }; + // Import Events so we can test + await _redcapApi.ImportEventsAsync(_token, Override.False, ReturnFormat.json, listOfEvents, OnErrorFormat.json); + // we want to export arm 1 and arm 2 + var exportArms = new string[] { "1", "2" }; + // Act + var result = await _redcapApi.ExportArmsAsync(_token, Content.Arm, ReturnFormat.json, exportArms); + + // Assert + // In order for the arms array to be returned, events for the specific arm + // needs to be present. An arm without any events will not be returned. + Assert.Contains("arm_num", result); + Assert.Contains("1", result); + Assert.Contains("2", result); + } + /// + /// Can import arms + /// + [Fact, TestPriority(0)] + public async void CanImportEventsAsync_Events_ShouldReturn_Number() + { + // Arrange + var listOfEvents = new List() { + new RedcapEvent{ + ArmNumber = "3", + CustomEventLabel = "HelloEvent1", + EventName = "Import Event 1", + DayOffset = "1", + MinimumOffset = "0", + MaximumOffset = "0", + UniqueEventName = "import_event_1_arm_3" + }, + new RedcapEvent{ + ArmNumber = "3", + CustomEventLabel = "HelloEvent2", + EventName = "Import Event 2", + DayOffset = "1", + MinimumOffset = "0", + MaximumOffset = "0", + UniqueEventName = "import_event_2_arm_3" + }, + new RedcapEvent{ + ArmNumber = "3", + CustomEventLabel = "HelloEvent3", + EventName = "Import Event 3", + DayOffset = "1", + MinimumOffset = "0", + MaximumOffset = "0", + UniqueEventName = "import_event_3_arm_3" + } + }; + + // Act + var result = await _redcapApi.ImportEventsAsync(_token, Override.False, ReturnFormat.json, listOfEvents, OnErrorFormat.json); + + // Assert + // Expecting 3 since we imported 3 events + Assert.Contains("3", result); + } + /// + /// Test attempts to import a file into the redcap project + /// There are a few assumptions, please make sure you have the files and folders + /// exactly as shown, or name it to your needs. + /// API Version 1.0.0+ + /// + [Fact, TestPriority(0)] + public async void CanImportFileAsync_File_ShouldReturn_Empty_string() + { + // Arrange + + var pathImport = "C:\\redcap_download_files"; + string importFileName = "test.txt"; + var record = "1"; + var fieldName = "file_upload"; + var eventName = "event_1_arm_1"; + var repeatingInstrument = "1"; + + // Act + var result = await _redcapApi.ImportFileAsync(_token, Content.File, RedcapAction.Import, record, fieldName, eventName, repeatingInstrument, importFileName, pathImport, OnErrorFormat.json); + + // Assert + // Expecting an empty string. This API returns an empty string on success. + Assert.True(string.IsNullOrEmpty(result)); + } + /// + /// Test attempts exports the file previously imported + /// + [Fact] + public async void CanExportFileAsync_File_ShouldReturn_string() + { + // Arrange + var pathExport = "C:\\redcap_download_files"; + var record = "1"; + var fieldName = "file_upload"; + var eventName = "event_1_arm_1"; + var repeatingInstrument = "1"; + var expectedString = "test.txt"; + var pathImport = "C:\\redcap_download_files"; + string importFileName = "test.txt"; + // we need to import a file first so we can test the export api + await _redcapApi.ImportFileAsync(_token, Content.File, RedcapAction.Import, record, fieldName, eventName, repeatingInstrument, importFileName, pathImport, OnErrorFormat.json); + + // Act + var result = await _redcapApi.ExportFileAsync(_token, Content.File, RedcapAction.Export, record, fieldName, eventName, repeatingInstrument, OnErrorFormat.json, pathExport); + + // Assert + Assert.Contains(expectedString, result); + } + /// + /// Can delete file previously uploaded + /// + [Fact] + public async void CanDeleteFileAsync_File_ShouldReturn_Empty_string() + { + // Arrange + + var pathImport = "C:\\redcap_download_files"; + string importFileName = "test.txt"; + var record = "1"; + var fieldName = "protocol_upload"; + // In order for us to import a file in a longitudinal format, we'll need to specify + // which event it belongs. + var eventName = "event_1_arm_1"; + var repeatingInstrument = "1"; + // Import a file to a record so we can do the integrated test below. + await _redcapApi.ImportFileAsync(_token, Content.File, RedcapAction.Import, record, fieldName, eventName, repeatingInstrument, importFileName, pathImport, OnErrorFormat.json); + + // Act + var result = await _redcapApi.DeleteFileAsync(_token, Content.File, RedcapAction.Delete, record, fieldName, eventName, repeatingInstrument, OnErrorFormat.json); + + // Assert + Assert.Contains(string.Empty, result); + } + /// + /// Can export records for projects that does not contain repeating forms/instruments + /// API Version 1.0.0+ + /// + [Fact] + public async void CanExportRecordsAsync_NonRepeating_Should_Return_String() + { + // Arrange + // Arrange + var data = new List> { }; + var keyValues = new Dictionary { }; + keyValues.Add("first_name", "Jon"); + keyValues.Add("last_name", "Doe"); + keyValues.Add("record_id", "8"); + data.Add(keyValues); + // import a record so we can test + await _redcapApi.ImportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, OverwriteBehavior.normal, false, data, "MDY", CsvDelimiter.comma, ReturnContent.count, OnErrorFormat.json); + + var records = new string[] { "8" }; + + // Act + var result = await _redcapApi.ExportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, records); + + // Assert + Assert.Contains("1", result); + } + /// + /// Can export records for project with repeating instruments/forms + /// API Version 1.0.0+ + /// + [Fact] + public async void CanExportRecordsAsync_Repeating_Should_Return_Record() + { + // Arrange + var repeatingInstruments = new List { + new RedcapRepeatInstrument { + EventName = "event_1_arm_1", + FormName = "demographics", + CustomFormLabel = "TestTestTest" + } + }; + + // We import a repeating instrument to 'turn on' repeating instruments feature + // as well as setting an initial repeating instrument + await _redcapApi.ImportRepeatingInstrumentsAndEvents(_token, repeatingInstruments); + // Get the redcap event from above + var redcapEvent = repeatingInstruments.FirstOrDefault(); + var data = new List> { }; + // passing in minimum requirements for a project with repeating instruments/forms + var keyValues = new Dictionary { }; + keyValues.Add("first_name", "Jon"); + keyValues.Add("last_name", "Doe"); + keyValues.Add("record_id", "8"); + keyValues.Add("redcap_repeat_instance", "1"); + keyValues.Add("redcap_repeat_instrument", redcapEvent.FormName); + data.Add(keyValues); + // import a record so we can test + await _redcapApi.ImportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, OverwriteBehavior.normal, false, data, "MDY", CsvDelimiter.comma, ReturnContent.count, OnErrorFormat.json); + var records = new string[] { "8" }; + + // Act + var result = await _redcapApi.ExportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, records); + + // Assert + Assert.Contains("1", result); + } + } +} diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index fc48088..771141a 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -1,7 +1,7 @@ - netcoreapp2.0 + net5.0 false @@ -11,14 +11,20 @@ - - - - - all - runtime; build; native; contentfiles; analyzers - - + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/ram_crest_160.png b/ram_crest_160.png deleted file mode 100644 index 90c3c9b508fbf01215b8da6c8816c984249cfbbb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12267 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 FV1fy{-FW~2 diff --git a/vcu.png b/vcu.png new file mode 100644 index 0000000000000000000000000000000000000000..11ec984db1a30eed0f9d72f2f80289b354ad05d5 GIT binary patch literal 8467 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 BzYqWb literal 0 HcmV?d00001 From c38c5db96f0ff8fa0f739853f4293926e0af0118 Mon Sep 17 00:00:00 2001 From: Michael Tran Date: Wed, 3 Feb 2021 23:01:04 -0500 Subject: [PATCH 02/13] Add new content enum type --- RedcapApi/Models/Content.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RedcapApi/Models/Content.cs b/RedcapApi/Models/Content.cs index 6687682..bdc20b8 100644 --- a/RedcapApi/Models/Content.cs +++ b/RedcapApi/Models/Content.cs @@ -8,6 +8,11 @@ namespace Redcap.Models /// public enum Content { + /// + /// Dag Content + /// + [Display(Name = "dag")] + Dag, /// /// Arm Content /// From 1bcfe97d5f10dac6a8f33899fc0700b67a2e4c2d Mon Sep 17 00:00:00 2001 From: Michael Tran Date: Wed, 3 Feb 2021 23:50:06 -0500 Subject: [PATCH 03/13] Added: -- Export Dags -- Import Dags -- Delete Dags -- Export User-DAG Assignment -- Import User-DAG Assignment --- RedcapApi/Api/Redcap.cs | 261 ++++++++++++++++++++++++++++++++++-- RedcapApi/Models/Action.cs | 2 +- RedcapApi/Models/Content.cs | 2 + 3 files changed, 255 insertions(+), 10 deletions(-) diff --git a/RedcapApi/Api/Redcap.cs b/RedcapApi/Api/Redcap.cs index 034b855..d14eebf 100644 --- a/RedcapApi/Api/Redcap.cs +++ b/RedcapApi/Api/Redcap.cs @@ -1,14 +1,18 @@ -using Newtonsoft.Json; -using Redcap.Interfaces; -using Redcap.Models; -using Redcap.Utilities; -using Serilog; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; + +using Newtonsoft.Json; + +using Redcap.Interfaces; +using Redcap.Models; +using Redcap.Utilities; + +using Serilog; + using static System.String; namespace Redcap @@ -78,7 +82,246 @@ public RedcapApi(string redcapApiUrl, bool useInsecureCertificates = false) } #region API Version 1.0.0+ Begin + #region Data Access Groups + /// + /// POST + /// Export DAGs + /// This method allows you to export the Data Access Groups for a project + /// + /// To use this method, you must have API Export privileges in the project. + /// + /// + /// The API token specific to your REDCap project and username (each token is unique to each user for each project). See the section on the left-hand menu for obtaining a token for a given project. + /// dag + /// csv, json [default], xml + /// csv, json, xml - specifies the format of error messages. If you do not pass in this flag, it will select the default format for you passed based on the 'format' flag you passed in or if no format flag was passed in, it will default to 'json'. + /// DAGs for the project in the format specified + public async Task ExportDagsAsync(string token, Content content, ReturnFormat format = ReturnFormat.json, OnErrorFormat onErrorFormat = OnErrorFormat.json) + { + var exportDagsResults = string.Empty; + try + { + // Check for presence of token + this.CheckToken(token); + + // Request payload + var payload = new Dictionary + { + { "token", token }, + { "content", Content.Dag.GetDisplayName() }, + { "format", format.GetDisplayName() }, + { "returnFormat", onErrorFormat.GetDisplayName() } + }; + exportDagsResults = await this.SendPostRequestAsync(payload, _uri); + return exportDagsResults; + + } + catch (Exception Ex) + { + /* + * We'll just log the error and return the error message. + */ + Log.Error($"{Ex.Message}"); + return exportDagsResults; + } + } + /// + /// POST + /// Import DAGs + /// This method allows you to import new DAGs (Data Access Groups) into a project or update the group name of any existing DAGs. + /// NOTE: DAGs can be renamed by simply changing the group name(data_access_group_name). DAG can be created by providing group name value while unique group name should be set to blank. + /// + /// + /// To use this method, you must have API Import/Update privileges in the project. + /// + /// The API token specific to your REDCap project and username (each token is unique to each user for each project). See the section on the left-hand menu for obtaining a token for a given project. + /// dags + /// import + /// csv, json [default], xml + /// Contains the attributes 'data_access_group_name' (referring to the group name) and 'unique_group_name' (referring to the auto-generated unique group name) of each DAG to be created/modified, in which they are provided in the specified format. + /// Refer to the API documenations for additional examples. + /// JSON Example: + /// [{"data_access_group_name":"CA Site","unique_group_name":"ca_site"} + /// {"data_access_group_name":"FL Site","unique_group_name":"fl_site"}, + /// { "data_access_group_name":"New Site","unique_group_name":""}] + /// CSV Example: + /// data_access_group_name,unique_group_name + /// "CA Site",ca_site + /// "FL Site",fl_site + /// "New Site", + /// + /// csv, json, xml - specifies the format of error messages. If you do not pass in this flag, it will select the default format for you passed based on the 'format' flag you passed in or if no format flag was passed in, it will default to 'json'. + /// Number of DAGs added or updated + public async Task ImportDagsAsync(string token, Content content, RedcapAction action, ReturnFormat format, List data, OnErrorFormat onErrorFormat = OnErrorFormat.json) + { + var importDagsResults = string.Empty; + try + { + // Check for presence of token + this.CheckToken(token); + + var _serializedData = JsonConvert.SerializeObject(data); + var payload = new Dictionary + { + { "token", token }, + { "content", content.GetDisplayName() }, + { "action", action.GetDisplayName() }, + { "format", format.GetDisplayName() }, + { "returnFormat", onErrorFormat.GetDisplayName() }, + { "data", _serializedData } + }; + // Execute request + return await this.SendPostRequestAsync(payload, _uri); + } + catch (Exception Ex) + { + Log.Error($"{Ex.Message}"); + return importDagsResults; + } + } + /// + /// POST + /// Delete DAGs + /// This method allows you to delete DAGs from a project. + /// + /// To use this method, you must have API Import/Update privileges in the project. + /// + /// + /// The API token specific to your REDCap project and username (each token is unique to each user for each project). See the section on the left-hand menu for obtaining a token for a given project. + /// dag + /// delete + /// an array of unique group names that you wish to delete + /// Number of DAGs deleted + public async Task DeleteDagsAsync(string token, Content content, RedcapAction action, string[] dags) + { + var deleteDagsResult = string.Empty; + try + { + // Check for presence of token + this.CheckToken(token); + // Check for any dags + if (dags.Length < 1) + { + throw new InvalidOperationException($"No dags to delete."); + } + var payload = new Dictionary + { + { "token", token }, + { "content", content.GetDisplayName() }, + { "action", action.GetDisplayName() } + }; + // Required + for (var i = 0; i < dags.Length; i++) + { + payload.Add($"dags[{i}]", dags[i].ToString()); + } + // Execute request + deleteDagsResult = await this.SendPostRequestAsync(payload, _uri); + return deleteDagsResult; + } + catch (Exception Ex) + { + Log.Error($"{Ex.Message}"); + return deleteDagsResult; + } + } + /// + /// POST + /// Export User-DAG Assignments + /// This method allows you to export existing User-DAG assignments for a project + /// + /// To use this method, you must have API Export privileges in the project. + /// + /// + /// The API token specific to your REDCap project and username (each token is unique to each user for each project). See the section on the left-hand menu for obtaining a token for a given project. + /// userDagMapping + /// csv, json [default], xml + /// csv, json, xml - specifies the format of error messages. If you do not pass in this flag, it will select the default format for you passed based on the 'format' flag you passed in or if no format flag was passed in, it will default to 'json'. + /// User-DAG assignments for the project in the format specified + public async Task ExportUserDagAssignmentAsync(string token, Content content, ReturnFormat format = ReturnFormat.json, OnErrorFormat onErrorFormat = OnErrorFormat.json) + { + var exportUserDagAssignmentResult = string.Empty; + try + { + // Check for presence of token + this.CheckToken(token); + + // Request payload + var payload = new Dictionary + { + { "token", token }, + { "content", content.GetDisplayName() }, + { "format", format.GetDisplayName() }, + { "returnFormat", onErrorFormat.GetDisplayName() } + }; + exportUserDagAssignmentResult = await this.SendPostRequestAsync(payload, _uri); + return exportUserDagAssignmentResult; + + } + catch (Exception Ex) + { + Log.Error($"{Ex.Message}"); + return exportUserDagAssignmentResult; + } + } + /// + /// POST + /// Import User-DAG Assignments + /// This method allows you to assign users to any data access group. + /// NOTE: If you wish to modify an existing mapping, you *must* provide its unique username and group name.If the 'redcap_data_access_group' column is not provided, user will not assigned to any group.There should be only one record per username. + /// + /// To use this method, you must have API Import/Update privileges in the project. + /// + /// + /// + /// The API token specific to your REDCap project and username (each token is unique to each user for each project). See the section on the left-hand menu for obtaining a token for a given project. + /// userDagMapping + /// import + /// csv, json [default], xml + /// + /// Contains the attributes 'username' (referring to the existing unique username) and 'redcap_data_access_group' (referring to existing unique group name) of each User-DAG assignments to be modified, in which they are provided in the specified format. + /// JSON Example: + /// [{"username":"ca_dt_person","redcap_data_access_group":"ca_site"}, + /// {"username":"fl_dt_person","redcap_data_access_group":"fl_site"}, + /// { "username":"global_user","redcap_data_access_group":""}] + /// CSV Example: + /// username,redcap_data_access_group + /// ca_dt_person, ca_site + /// fl_dt_person, fl_site + /// global_user, + /// + /// csv, json, xml - specifies the format of error messages. If you do not pass in this flag, it will select the default format for you passed based on the 'format' flag you passed in or if no format flag was passed in, it will default to 'json'. + /// Number of User-DAG assignments added or updated + public async Task ImportUserDagAssignmentAsync(string token, Content content, RedcapAction action, ReturnFormat format, List data, OnErrorFormat onErrorFormat = OnErrorFormat.json) + { + var ImportUserDagAssignmentResults = string.Empty; + try + { + // Check for presence of token + this.CheckToken(token); + + var _serializedData = JsonConvert.SerializeObject(data); + var payload = new Dictionary + { + { "token", token }, + { "content", content.GetDisplayName() }, + { "action", action.GetDisplayName() }, + { "format", format.GetDisplayName() }, + { "returnFormat", onErrorFormat.GetDisplayName() }, + { "data", _serializedData } + }; + // Execute request + return await this.SendPostRequestAsync(payload, _uri); + } + catch (Exception Ex) + { + Log.Error($"{Ex.Message}"); + return ImportUserDagAssignmentResults; + } + } + + #endregion #region Arms /// /// API Version 1.0.0+ ** @@ -221,7 +464,7 @@ public async Task ExportArmsAsync(string token, Content content, ReturnF /// /// csv, json, xml - specifies the format of error messages. If you do not pass in this flag, it will select the default format for you passed based on the 'format' flag you passed in or if no format flag was passed in, it will default to 'xml'. /// Number of Arms imported - public async Task ImportArmsAsync(string token, Override overrideBhavior, RedcapAction action, ReturnFormat format, List data, OnErrorFormat returnFormat) + public async Task ImportArmsAsync(string token, Override overrideBhavior, RedcapAction action, ReturnFormat format, List data, OnErrorFormat returnFormat = OnErrorFormat.json) { try { @@ -3628,7 +3871,7 @@ public async Task ExportSurveyParticipantsAsync(string token, Content co return Ex.Message; } } - + /// /// API Version 1.0.0+ /// From Redcap Version 6.4.0 @@ -3713,7 +3956,7 @@ public async Task ExportSurveyQueueLinkAsync(string token, Content conte return Ex.Message; } } - + /// /// API Version 1.0.0+ /// From Redcap Version 6.4.0 diff --git a/RedcapApi/Models/Action.cs b/RedcapApi/Models/Action.cs index 9386795..2bfb29b 100644 --- a/RedcapApi/Models/Action.cs +++ b/RedcapApi/Models/Action.cs @@ -7,7 +7,7 @@ namespace Redcap.Models { /// - /// API Action + /// API Action => Export, Import, Delete /// public enum RedcapAction { diff --git a/RedcapApi/Models/Content.cs b/RedcapApi/Models/Content.cs index bdc20b8..2b46c04 100644 --- a/RedcapApi/Models/Content.cs +++ b/RedcapApi/Models/Content.cs @@ -8,6 +8,8 @@ namespace Redcap.Models /// public enum Content { + [Display(Name = "userDagMapping")] + UserDagMapping, /// /// Dag Content /// From 59b9caa0806f19b4dac1d153520c510681074308 Mon Sep 17 00:00:00 2001 From: Michael Tran Date: Thu, 4 Feb 2021 00:06:47 -0500 Subject: [PATCH 04/13] Add new enum type for logging api --- RedcapApi/Models/LogType.cs | 70 +++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 RedcapApi/Models/LogType.cs diff --git a/RedcapApi/Models/LogType.cs b/RedcapApi/Models/LogType.cs new file mode 100644 index 0000000..0e52354 --- /dev/null +++ b/RedcapApi/Models/LogType.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Text; + +namespace Redcap.Models +{ + public enum LogType + { + /// + /// Data Export + /// + [Display(Name = "export")] + Export, + + /// + /// Manage/Design + /// + [Display(Name = "manage")] + Manage, + + /// + /// User or role created-updated-deleted + /// + [Display(Name = "user")] + User, + + /// + /// Record created-updated-deleted + /// + [Display(Name = "record")] + Record, + + /// + /// Record created (only) + /// + [Display(Name = "record_add")] + RecordAdd, + + /// + /// Record updated (only) + /// + [Display(Name = "record_edit")] + RecordEdit, + + /// + /// Record deleted (only) + /// + [Display(Name = "record_delete")] + RecordDelete, + + /// + /// Record locking and e-signatures + /// + [Display(Name ="lock_record")] + LockRecord, + /// + /// Page views + /// + /// + [Display(Name = "page_view")] + PageView, + /// + /// All event types (excluding page views) + /// + [Display(Name = "")] + All + } +} From c9b9e0f3928f5c9d93d1530175ad0753843a9b2f Mon Sep 17 00:00:00 2001 From: Michael Tran Date: Thu, 4 Feb 2021 00:07:20 -0500 Subject: [PATCH 05/13] add new content type --- RedcapApi/Models/Content.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/RedcapApi/Models/Content.cs b/RedcapApi/Models/Content.cs index 2b46c04..4f6cf1f 100644 --- a/RedcapApi/Models/Content.cs +++ b/RedcapApi/Models/Content.cs @@ -8,6 +8,14 @@ namespace Redcap.Models /// public enum Content { + /// + /// Log + /// + [Display(Name ="log")] + Log, + /// + /// User-Mapping + /// [Display(Name = "userDagMapping")] UserDagMapping, /// From e728ed0494adaea0ecd4fa5343e7bd54f6dc659d Mon Sep 17 00:00:00 2001 From: Michael Tran Date: Thu, 4 Feb 2021 00:07:55 -0500 Subject: [PATCH 06/13] comment --- RedcapApi/Models/LogType.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/RedcapApi/Models/LogType.cs b/RedcapApi/Models/LogType.cs index 0e52354..4c18153 100644 --- a/RedcapApi/Models/LogType.cs +++ b/RedcapApi/Models/LogType.cs @@ -1,11 +1,10 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; -using System.Text; +using System.ComponentModel.DataAnnotations; namespace Redcap.Models { + /// + /// Logging type + /// public enum LogType { /// From ff4ae4fdfb25f1657f392d6cbbaffa2b859144d0 Mon Sep 17 00:00:00 2001 From: Michael Tran Date: Thu, 4 Feb 2021 00:24:18 -0500 Subject: [PATCH 07/13] add ExportLoggingAsync --- RedcapApi/Api/Redcap.cs | 74 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/RedcapApi/Api/Redcap.cs b/RedcapApi/Api/Redcap.cs index d14eebf..9a7ef5a 100644 --- a/RedcapApi/Api/Redcap.cs +++ b/RedcapApi/Api/Redcap.cs @@ -82,6 +82,75 @@ public RedcapApi(string redcapApiUrl, bool useInsecureCertificates = false) } #region API Version 1.0.0+ Begin + #region Logging + /// + /// API @Version 10.8 + /// POST + /// Export Logging + /// This method allows you to export the logging (audit trail) of all changes made to this project, including data exports, data changes, project metadata changes, modification of user rights, etc. + /// + /// To use this method, you must have API Export privileges in the project. + /// + /// + /// The API token specific to your REDCap project and username (each token is unique to each user for each project). See the section on the left-hand menu for obtaining a token for a given project. + /// log + /// csv, json [default], xml + /// You may choose event type to fetch result for specific event type + /// To return only the events belong to specific user (referring to existing username), provide a user. If not specified, it will assume all users + /// To return only the events belong to specific record (referring to existing record name), provide a record. If not specified, it will assume all records. This parameter is available only when event is related to record. + /// To return only the events belong to specific DAG (referring to group_id), provide a dag. If not specified, it will assume all dags. + /// To return only the events that have been logged *after* a given date/time, provide a timestamp in the format YYYY-MM-DD HH:MM (e.g., '2017-01-01 17:00' for January 1, 2017 at 5:00 PM server time). If not specified, it will assume no begin time. + /// To return only records that have been logged *before* a given date/time, provide a timestamp in the format YYYY-MM-DD HH:MM (e.g., '2017-01-01 17:00' for January 1, 2017 at 5:00 PM server time). If not specified, it will use the current server time. + /// csv, json, xml - specifies the format of error messages. If you do not pass in this flag, it will select the default format for you passed based on the 'format' flag you passed in or if no format flag was passed in, it will default to 'json'. + /// List of all changes made to this project, including data exports, data changes, and the creation or deletion of users. + public async Task ExportLoggingAsync(string token, Content content, ReturnFormat format = ReturnFormat.json, LogType logType = LogType.All, string user = null, string record = null, string dag = null, string beginTime = null, string endTime = null, OnErrorFormat onErrorFormat = OnErrorFormat.json) + { + var exportLoggingResults = string.Empty; + try + { + // Check for presence of token + this.CheckToken(token); + + var payload = new Dictionary + { + { "token", token }, + { "content", content.GetDisplayName() }, + { "format", format.GetDisplayName() }, + { "returnFormat", onErrorFormat.GetDisplayName() }, + { "logtype", logType.GetDisplayName() } + }; + + // Optional + if (!string.IsNullOrEmpty(user)) + { + payload.Add("user", user); + } + if (!string.IsNullOrEmpty(record)) + { + payload.Add("record", record); + } + if (!string.IsNullOrEmpty(dag)) + { + payload.Add("dag", dag); + } + if (!string.IsNullOrEmpty(beginTime)) + { + payload.Add("beginTime", beginTime); + } + if (!string.IsNullOrEmpty(endTime)) + { + payload.Add("endTime", endTime); + } + exportLoggingResults = await this.SendPostRequestAsync(payload, _uri); + return exportLoggingResults; + } + catch (Exception Ex) + { + Log.Error($"{Ex.Message}"); + return exportLoggingResults; + } + } + #endregion #region Data Access Groups /// /// POST @@ -108,7 +177,7 @@ public async Task ExportDagsAsync(string token, Content content, ReturnF var payload = new Dictionary { { "token", token }, - { "content", Content.Dag.GetDisplayName() }, + { "content", content.GetDisplayName() }, { "format", format.GetDisplayName() }, { "returnFormat", onErrorFormat.GetDisplayName() } }; @@ -118,9 +187,6 @@ public async Task ExportDagsAsync(string token, Content content, ReturnF } catch (Exception Ex) { - /* - * We'll just log the error and return the error message. - */ Log.Error($"{Ex.Message}"); return exportDagsResults; } From c0f6154aac05daa018834cb152f65d7c0dd2cfa6 Mon Sep 17 00:00:00 2001 From: Michael Tran Date: Thu, 4 Feb 2021 00:31:43 -0500 Subject: [PATCH 08/13] Update Redcap.csproj --- RedcapApi/Redcap.csproj | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/RedcapApi/Redcap.csproj b/RedcapApi/Redcap.csproj index 6919266..f4cd799 100644 --- a/RedcapApi/Redcap.csproj +++ b/RedcapApi/Redcap.csproj @@ -10,20 +10,24 @@ This library allows applications on the .NET platform to make http calls to REDCap instances. Redcap Api Library RedcapAPI - 1.0.9 - 1.0.9.0 + 1.1.0 + 1.1.0.0 redcap api library vcu - - add improvements to 'ImportRecordsAsync' - Add ability to declare csv delimiter to match REDCap's API paramters -- minor update documentation -- Minor version bump - + New APIs have been added! +-ExportLoggingAsync +-ExportDagsAsync +-ImportDagsAsync +-DeleteDagsAsync +-ExportUserDagAssignmentAsync +-ImportUserDagAssignmentAsync +Minor bugs fixes to existing APIs regarding optional parameters. +Version bump false Library en - 1.0.9.0 + 1.1.0.0 https://github.com/cctrbic/redcap-api/blob/master/LICENSE.md https://github.com/cctrbic/redcap-api/blob/master/LICENSE.md https://vortex.cctr.vcu.edu/images/ram_crest_160.png @@ -32,7 +36,7 @@ false - bin\Debug\netcoreapp2.0\Redcap.xml + portable true AnyCPU From 4a96413162bac4953cb67f1ab3643a060530003b Mon Sep 17 00:00:00 2001 From: Michael Tran Date: Thu, 4 Feb 2021 00:39:02 -0500 Subject: [PATCH 09/13] Update README.md update version and minor tweaks --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e8335c2..fa1f9bd 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,10 @@ __Prerequisites__ 5. Build the solution, then run the tests __API METHODS SUPPORTED (Not all listed)__ +* ExportLoggingAsync +* ExportDagsAsync +* ImportDagsAsync +* DeleteDagsAsync * ExportArmsAsync * ImportArmsAsync * DeleteArmsAsync @@ -54,7 +58,7 @@ namespace RedcapApiDemo Console.WriteLine("Redcap Api Demo Started!"); // Use your own API Token here... var apiToken = "3D57A7FA57C8A43F6C8803A84BB3957B"; - var redcap_api = new RedcapApi("http://localhost/redcap/api/"); + var redcap_api = new RedcapApi("https://localhost/redcap/api/"); Console.WriteLine("Exporting all records from project."); var result = redcap_api.ExportRecordsAsync(apiToken).Result; @@ -73,21 +77,21 @@ __Install directly in Package Manager Console or Command Line Interface__ ```C# Package Manager -Install-Package RedcapAPI -Version 1.0.9 +Install-Package RedcapAPI -Version 1.1.0 ``` ```C# .NET CLI -dotnet add package RedcapAPI --version 1.0.9 +dotnet add package RedcapAPI --version 1.1.0 ``` ```C# Paket CLI -paket add RedcapAPI --version 1.0.9 +paket add RedcapAPI --version 1.1.0 ``` From 9c2be5ee018f6b7b86dc9479bb99ce7b839ffe6d Mon Sep 17 00:00:00 2001 From: Michael Tran Date: Thu, 4 Feb 2021 10:00:25 -0500 Subject: [PATCH 10/13] update interface --- RedcapApi/Interfaces/IRedcap.cs | 6 ++++++ RedcapApiDemo/Program.cs | 13 ++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/RedcapApi/Interfaces/IRedcap.cs b/RedcapApi/Interfaces/IRedcap.cs index fb6823e..6ecc793 100644 --- a/RedcapApi/Interfaces/IRedcap.cs +++ b/RedcapApi/Interfaces/IRedcap.cs @@ -1484,5 +1484,11 @@ public interface IRedcap /// MDY, DMY, YMD [default] - the format of values being imported for dates or datetime fields (understood with M representing 'month', D as 'day', and Y as 'year') - NOTE: The default format is Y-M-D (with dashes), while MDY and DMY values should always be formatted as M/D/Y or D/M/Y (with slashes), respectively. /// Returns the content with format specified. Task SaveRecordsAsync(List data, ReturnContent returnContent, OverwriteBehavior overwriteBehavior, ReturnFormat? inputFormat, RedcapDataType? redcapDataType, OnErrorFormat? returnFormat, string dateFormat = "MDY"); + Task ExportLoggingAsync(string token, Content content, ReturnFormat format = ReturnFormat.json, LogType logType = LogType.All, string user = null, string record = null, string dag = null, string beginTime = null, string endTime = null, OnErrorFormat onErrorFormat = OnErrorFormat.json); + Task ExportDagsAsync(string token, Content content, ReturnFormat format = ReturnFormat.json, OnErrorFormat onErrorFormat = OnErrorFormat.json); + Task ImportDagsAsync(string token, Content content, RedcapAction action, ReturnFormat format, List data, OnErrorFormat onErrorFormat = OnErrorFormat.json); + Task DeleteDagsAsync(string token, Content content, RedcapAction action, string[] dags); + Task ExportUserDagAssignmentAsync(string token, Content content, ReturnFormat format = ReturnFormat.json, OnErrorFormat onErrorFormat = OnErrorFormat.json); + Task ImportUserDagAssignmentAsync(string token, Content content, RedcapAction action, ReturnFormat format, List data, OnErrorFormat onErrorFormat = OnErrorFormat.json); } } diff --git a/RedcapApiDemo/Program.cs b/RedcapApiDemo/Program.cs index 0484ae1..d87a990 100644 --- a/RedcapApiDemo/Program.cs +++ b/RedcapApiDemo/Program.cs @@ -1,10 +1,12 @@ -using Newtonsoft.Json; -using Redcap; -using Redcap.Models; -using System; +using System; using System.Collections.Generic; using System.Linq; +using Newtonsoft.Json; + +using Redcap; +using Redcap.Models; + namespace RedcapApiDemo { public class Demographic @@ -266,7 +268,7 @@ static void InitializeDemo() } }; Console.WriteLine($"Importing record {string.Join(",", data.Select(x => x.RecordId).ToList())} . . ."); - var ImportRecordsAsync = redcap_api_1_0_7.ImportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, OverwriteBehavior.normal, false, data, "MDY", CsvDelimiter.tab, ReturnContent.count, OnErrorFormat.json).Result; + var ImportRecordsAsync = redcap_api_1_0_7.ImportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, OverwriteBehavior.normal, false, data, "MDY", CsvDelimiter.tab, ReturnContent.count, OnErrorFormat.json).Result; var ImportRecordsAsyncData = JsonConvert.DeserializeObject(ImportRecordsAsync); Console.WriteLine($"ImportRecordsAsync Result: {ImportRecordsAsyncData}"); #endregion ImportRecordsAsync() @@ -286,6 +288,7 @@ static void InitializeDemo() Console.WriteLine($"DeleteRecordsAsync Result: {DeleteRecordsAsync}"); #endregion DeleteRecordsAsync() + #region ExportArmsAsync() var arms = new string[] { }; Console.WriteLine("Calling ExportArmsAsync()"); From de21468765ec39abdb070741736646b065dc6ed7 Mon Sep 17 00:00:00 2001 From: Michael Tran Date: Thu, 4 Feb 2021 10:24:24 -0500 Subject: [PATCH 11/13] Update Program.cs --- RedcapApiDemo/Program.cs | 80 +++++++++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/RedcapApiDemo/Program.cs b/RedcapApiDemo/Program.cs index d87a990..ac51c7f 100644 --- a/RedcapApiDemo/Program.cs +++ b/RedcapApiDemo/Program.cs @@ -9,6 +9,10 @@ namespace RedcapApiDemo { + /// + /// A class that represents the demopgrahics form for the demographics template. + /// Add additional properties that you've added to the redcap instrument as needed. + /// public class Demographic { [JsonRequired] @@ -226,7 +230,7 @@ static void InitializeDemo() * Change this token for your demo project * Using one created from a local dev instance */ - string _superToken = "92F719F0EC97783D06B0E0FF49DC42DDA2247BFDC6759F324EE0D710FCA87C33"; + string _superToken = "2E59CA118ABC17D393722524C501CF0BAC51689746E24BFDAF47B38798BD827A"; /* * Using a local redcap development instsance */ @@ -241,10 +245,24 @@ static void InitializeDemo() Console.WriteLine("Please make sure you include a working redcap api token."); Console.WriteLine("Enter your redcap instance uri, example: http://localhost/redcap"); _uri = Console.ReadLine(); + if (string.IsNullOrEmpty(_uri)) + { + // provide a default one here.. + _uri = "http://localhost/redcap"; + } _uri = _uri + "/api/"; Console.WriteLine("Enter your api token for the project to test: "); var token = Console.ReadLine(); - _token = token; + + if (string.IsNullOrEmpty(token)) + { + _token = "93DC9C0B48C9F7C26DF67B849B1A4124"; + } + else + { + _token = token; + } + Console.WriteLine($"Using Endpoint=> {_uri} Token => {_token}"); Console.WriteLine("-----------------------------Starting API Version 1.0.5+-------------"); Console.WriteLine("Starting demo for API Version 1.0.0+"); @@ -252,10 +270,20 @@ static void InitializeDemo() Console.ReadLine(); Console.WriteLine("Creating a new instance of RedcapApi"); - var redcap_api_1_0_7 = new RedcapApi(_uri); + var redcap_api_1_1_0 = new RedcapApi(_uri); Console.WriteLine($"Using {_uri.ToString()} for redcap api endpoint."); + #region ExportLoggingAsync() + Console.WriteLine("Calling ExportLoggingAsync() . . ."); + Console.WriteLine($"Exporting logs for User . . ."); + var ExportLoggingAsync = redcap_api_1_1_0.ExportLoggingAsync(_token, Content.Log, ReturnFormat.json, LogType.User).Result; + Console.WriteLine($"ExportLoggingAsync Results: {JsonConvert.DeserializeObject(ExportLoggingAsync)}"); + Console.WriteLine("----------------------------Press Enter to Continue-------------"); + Console.ReadLine(); + + #endregion + #region ImportRecordsAsync() Console.WriteLine("Calling ImportRecordsAsync() . . ."); /* @@ -268,7 +296,7 @@ static void InitializeDemo() } }; Console.WriteLine($"Importing record {string.Join(",", data.Select(x => x.RecordId).ToList())} . . ."); - var ImportRecordsAsync = redcap_api_1_0_7.ImportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, OverwriteBehavior.normal, false, data, "MDY", CsvDelimiter.tab, ReturnContent.count, OnErrorFormat.json).Result; + var ImportRecordsAsync = redcap_api_1_1_0.ImportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, OverwriteBehavior.normal, false, data, "MDY", CsvDelimiter.tab, ReturnContent.count, OnErrorFormat.json).Result; var ImportRecordsAsyncData = JsonConvert.DeserializeObject(ImportRecordsAsync); Console.WriteLine($"ImportRecordsAsync Result: {ImportRecordsAsyncData}"); #endregion ImportRecordsAsync() @@ -280,7 +308,7 @@ static void InitializeDemo() Console.WriteLine("Calling DeleteRecordsAsync() . . ."); var records = new string[] { "1" }; Console.WriteLine($"Deleting record {string.Join(",", records)} . . ."); - var DeleteRecordsAsync = redcap_api_1_0_7.DeleteRecordsAsync(_token, Content.Record, RedcapAction.Delete, records, 1).Result; + var DeleteRecordsAsync = redcap_api_1_1_0.DeleteRecordsAsync(_token, Content.Record, RedcapAction.Delete, records, 1).Result; Console.WriteLine("----------------------------Press Enter to Continue-------------"); Console.ReadLine(); @@ -292,7 +320,7 @@ static void InitializeDemo() #region ExportArmsAsync() var arms = new string[] { }; Console.WriteLine("Calling ExportArmsAsync()"); - var ExportArmsAsyncResult = redcap_api_1_0_7.ExportArmsAsync(_token, Content.Arm, ReturnFormat.json, arms, OnErrorFormat.json).Result; + var ExportArmsAsyncResult = redcap_api_1_1_0.ExportArmsAsync(_token, Content.Arm, ReturnFormat.json, arms, OnErrorFormat.json).Result; Console.WriteLine($"ExportArmsAsyncResult: {ExportArmsAsyncResult}"); #endregion ExportArmsAsync() @@ -302,7 +330,7 @@ static void InitializeDemo() #region ImportArmsAsync() var ImportArmsAsyncData = new List { new RedcapArm { ArmNumber = "1", Name = "hooo" }, new RedcapArm { ArmNumber = "2", Name = "heee" }, new RedcapArm { ArmNumber = "3", Name = "hawww" } }; Console.WriteLine("Calling ImportArmsAsync()"); - var ImportArmsAsyncResult = redcap_api_1_0_7.ImportArmsAsync(_token, Content.Arm, Override.False, RedcapAction.Import, ReturnFormat.json, ImportArmsAsyncData, OnErrorFormat.json).Result; + var ImportArmsAsyncResult = redcap_api_1_1_0.ImportArmsAsync(_token, Content.Arm, Override.False, RedcapAction.Import, ReturnFormat.json, ImportArmsAsyncData, OnErrorFormat.json).Result; Console.WriteLine($"ImportArmsAsyncResult: {ImportArmsAsyncResult}"); #endregion ImportArmsAsync() @@ -312,7 +340,7 @@ static void InitializeDemo() #region DeleteArmsAsync() var DeleteArmsAsyncData = new string[] { "3" }; Console.WriteLine("Calling DeleteArmsAsync()"); - var DeleteArmsAsyncResult = redcap_api_1_0_7.DeleteArmsAsync(_token, Content.Arm, RedcapAction.Delete, DeleteArmsAsyncData).Result; + var DeleteArmsAsyncResult = redcap_api_1_1_0.DeleteArmsAsync(_token, Content.Arm, RedcapAction.Delete, DeleteArmsAsyncData).Result; Console.WriteLine($"DeleteArmsAsyncResult: {DeleteArmsAsyncResult}"); #endregion DeleteArmsAsync() @@ -322,7 +350,7 @@ static void InitializeDemo() #region ExportEventsAsync() var ExportEventsAsyncData = new string[] { "1" }; Console.WriteLine("Calling ExportEventsAsync()"); - var ExportEventsAsyncResult = redcap_api_1_0_7.ExportEventsAsync(_token, Content.Event, ReturnFormat.json, ExportEventsAsyncData, OnErrorFormat.json).Result; + var ExportEventsAsyncResult = redcap_api_1_1_0.ExportEventsAsync(_token, Content.Event, ReturnFormat.json, ExportEventsAsyncData, OnErrorFormat.json).Result; Console.WriteLine($"ExportEventsAsyncResult: {ExportEventsAsyncResult}"); #endregion ExportEventsAsync() @@ -351,7 +379,7 @@ static void InitializeDemo() CustomEventLabel = "hello clinical" } }; - var ImportEventsAsyncResult = redcap_api_1_0_7.ImportEventsAsync(_token, Content.Event, RedcapAction.Import, Override.False, ReturnFormat.json, eventList, OnErrorFormat.json).Result; + var ImportEventsAsyncResult = redcap_api_1_1_0.ImportEventsAsync(_token, Content.Event, RedcapAction.Import, Override.False, ReturnFormat.json, eventList, OnErrorFormat.json).Result; Console.WriteLine($"ImportEventsAsyncResult: {ImportEventsAsyncResult}"); #endregion ImportEventsAsync() @@ -362,7 +390,7 @@ static void InitializeDemo() #region DeleteEventsAsync() var DeleteEventsAsyncData = new string[] { "baseline_arm_1" }; Console.WriteLine("Calling DeleteEventsAsync()"); - var DeleteEventsAsyncResult = redcap_api_1_0_7.DeleteEventsAsync(_token, Content.Event, RedcapAction.Delete, DeleteEventsAsyncData).Result; + var DeleteEventsAsyncResult = redcap_api_1_1_0.DeleteEventsAsync(_token, Content.Event, RedcapAction.Delete, DeleteEventsAsyncData).Result; Console.WriteLine($"DeleteEventsAsyncResult: {DeleteEventsAsyncResult}"); #endregion DeleteEventsAsync() @@ -372,7 +400,7 @@ static void InitializeDemo() #region ExportFieldNamesAsync() Console.WriteLine("Calling ExportFieldNamesAsync(), first_name"); - var ExportFieldNamesAsyncResult = redcap_api_1_0_7.ExportFieldNamesAsync(_token, Content.ExportFieldNames, ReturnFormat.json, "first_name", OnErrorFormat.json).Result; + var ExportFieldNamesAsyncResult = redcap_api_1_1_0.ExportFieldNamesAsync(_token, Content.ExportFieldNames, ReturnFormat.json, "first_name", OnErrorFormat.json).Result; Console.WriteLine($"ExportFieldNamesAsyncResult: {ExportFieldNamesAsyncResult}"); #endregion ExportFieldNamesAsync() @@ -385,7 +413,7 @@ static void InitializeDemo() var fileName = "test.txt"; var fileUploadPath = @"C:\redcap_upload_files"; Console.WriteLine($"Calling ImportFileAsync(), {fileName}"); - var ImportFileAsyncResult = redcap_api_1_0_7.ImportFileAsync(_token, Content.File, RedcapAction.Import, recordId, fieldName, eventName, null, fileName, fileUploadPath, OnErrorFormat.json).Result; + var ImportFileAsyncResult = redcap_api_1_1_0.ImportFileAsync(_token, Content.File, RedcapAction.Import, recordId, fieldName, eventName, null, fileName, fileUploadPath, OnErrorFormat.json).Result; Console.WriteLine($"ImportFileAsyncResult: {ImportFileAsyncResult}"); #endregion ImportFileAsync() @@ -395,7 +423,7 @@ static void InitializeDemo() #region ExportFileAsync() Console.WriteLine($"Calling ExportFileAsync(), {fileName} for field name {fieldName}, not save the file."); - var ExportFileAsyncResult = redcap_api_1_0_7.ExportFileAsync(_token, Content.File, RedcapAction.Export, recordId, fieldName, eventName, null, OnErrorFormat.json).Result; + var ExportFileAsyncResult = redcap_api_1_1_0.ExportFileAsync(_token, Content.File, RedcapAction.Export, recordId, fieldName, eventName, null, OnErrorFormat.json).Result; Console.WriteLine($"ExportFileAsyncResult: {ExportFileAsyncResult}"); #endregion ExportFileAsync() @@ -405,7 +433,7 @@ static void InitializeDemo() #region ExportFileAsync() var filedDownloadPath = @"C:\redcap_download_files"; Console.WriteLine($"Calling ExportFileAsync(), {fileName} for field name {fieldName}, saving the file."); - var ExportFileAsyncResult2 = redcap_api_1_0_7.ExportFileAsync(_token, Content.File, RedcapAction.Export, recordId, fieldName, eventName, null, OnErrorFormat.json, filedDownloadPath).Result; + var ExportFileAsyncResult2 = redcap_api_1_1_0.ExportFileAsync(_token, Content.File, RedcapAction.Export, recordId, fieldName, eventName, null, OnErrorFormat.json, filedDownloadPath).Result; Console.WriteLine($"ExportFileAsyncResult2: {ExportFileAsyncResult2}"); #endregion ExportFileAsync() @@ -414,7 +442,7 @@ static void InitializeDemo() #region DeleteFileAsync() Console.WriteLine($"Calling DeleteFileAsync(), deleting file: {fileName} for field: {fieldName}"); - var DeleteFileAsyncResult = redcap_api_1_0_7.DeleteFileAsync(_token, Content.File, RedcapAction.Delete, recordId, fieldName, eventName, "1", OnErrorFormat.json).Result; + var DeleteFileAsyncResult = redcap_api_1_1_0.DeleteFileAsync(_token, Content.File, RedcapAction.Delete, recordId, fieldName, eventName, "1", OnErrorFormat.json).Result; Console.WriteLine($"DeleteFileAsyncResult: {DeleteFileAsyncResult}"); #endregion DeleteFileAsync() @@ -423,7 +451,7 @@ static void InitializeDemo() #region ExportInstrumentsAsync() Console.WriteLine($"Calling DeleteFileAsync()"); - var ExportInstrumentsAsyncResult = redcap_api_1_0_7.ExportInstrumentsAsync(_token, Content.Instrument, ReturnFormat.json).Result; + var ExportInstrumentsAsyncResult = redcap_api_1_1_0.ExportInstrumentsAsync(_token, Content.Instrument, ReturnFormat.json).Result; Console.WriteLine($"ExportInstrumentsAsyncResult: {ExportInstrumentsAsyncResult}"); #endregion ExportInstrumentsAsync() @@ -432,7 +460,7 @@ static void InitializeDemo() #region ExportPDFInstrumentsAsync() Console.WriteLine($"Calling ExportPDFInstrumentsAsync(), returns raw"); - var ExportPDFInstrumentsAsyncResult = redcap_api_1_0_7.ExportPDFInstrumentsAsync(_token, Content.Pdf, recordId, eventName, "demographics", true, OnErrorFormat.json).Result; + var ExportPDFInstrumentsAsyncResult = redcap_api_1_1_0.ExportPDFInstrumentsAsync(_token, Content.Pdf, recordId, eventName, "demographics", true, OnErrorFormat.json).Result; Console.WriteLine($"ExportInstrumentsAsyncResult: {JsonConvert.SerializeObject(ExportPDFInstrumentsAsyncResult)}"); #endregion ExportPDFInstrumentsAsync() @@ -441,7 +469,7 @@ static void InitializeDemo() #region ExportPDFInstrumentsAsync() Console.WriteLine($"Calling ExportPDFInstrumentsAsync(), saving pdf file to {filedDownloadPath}"); - var ExportPDFInstrumentsAsyncResult2 = redcap_api_1_0_7.ExportPDFInstrumentsAsync(_token, recordId, eventName, "demographics", true, filedDownloadPath, OnErrorFormat.json).Result; + var ExportPDFInstrumentsAsyncResult2 = redcap_api_1_1_0.ExportPDFInstrumentsAsync(_token, recordId, eventName, "demographics", true, filedDownloadPath, OnErrorFormat.json).Result; Console.WriteLine($"ExportPDFInstrumentsAsyncResult2: {ExportPDFInstrumentsAsyncResult2}"); #endregion ExportPDFInstrumentsAsync() @@ -450,7 +478,7 @@ static void InitializeDemo() #region ExportInstrumentMappingAsync() Console.WriteLine($"Calling ExportInstrumentMappingAsync()"); - var ExportInstrumentMappingAsyncResult = redcap_api_1_0_7.ExportInstrumentMappingAsync(_token, Content.FormEventMapping, ReturnFormat.json, arms, OnErrorFormat.json).Result; + var ExportInstrumentMappingAsyncResult = redcap_api_1_1_0.ExportInstrumentMappingAsync(_token, Content.FormEventMapping, ReturnFormat.json, arms, OnErrorFormat.json).Result; Console.WriteLine($"ExportInstrumentMappingAsyncResult: {ExportInstrumentMappingAsyncResult}"); #endregion ExportInstrumentMappingAsync() @@ -460,7 +488,7 @@ static void InitializeDemo() #region ImportInstrumentMappingAsync() var importInstrumentMappingData = new List { new FormEventMapping { arm_num = "1", unique_event_name = "clinical_arm_1", form = "demographics" } }; Console.WriteLine($"Calling ImportInstrumentMappingAsync()"); - var ImportInstrumentMappingAsyncResult = redcap_api_1_0_7.ImportInstrumentMappingAsync(_token, Content.FormEventMapping, ReturnFormat.json, importInstrumentMappingData, OnErrorFormat.json).Result; + var ImportInstrumentMappingAsyncResult = redcap_api_1_1_0.ImportInstrumentMappingAsync(_token, Content.FormEventMapping, ReturnFormat.json, importInstrumentMappingData, OnErrorFormat.json).Result; Console.WriteLine($"ImportInstrumentMappingAsyncResult: {ImportInstrumentMappingAsyncResult}"); #endregion ImportInstrumentMappingAsync() @@ -469,7 +497,7 @@ static void InitializeDemo() #region ExportMetaDataAsync() Console.WriteLine($"Calling ExportMetaDataAsync()"); - var ExportMetaDataAsyncResult = redcap_api_1_0_7.ExportMetaDataAsync(_token, Content.MetaData, ReturnFormat.json, null, null, OnErrorFormat.json).Result; + var ExportMetaDataAsyncResult = redcap_api_1_1_0.ExportMetaDataAsync(_token, Content.MetaData, ReturnFormat.json, null, null, OnErrorFormat.json).Result; Console.WriteLine($"ExportMetaDataAsyncResult: {ExportMetaDataAsyncResult}"); #endregion ExportMetaDataAsync() @@ -493,7 +521,7 @@ static void InitializeDemo() var projectData = new List { new RedcapProject { project_title = "Amazing Project ", purpose = ProjectPurpose.Other, purpose_other = "Test" } }; Console.WriteLine($"Calling CreateProjectAsync(), creating a new project with Amazing Project as title, purpose 1 (other) "); Console.WriteLine($"-----------------------Notice the use of SUPER TOKEN------------------------"); - var CreateProjectAsyncResult = redcap_api_1_0_7.CreateProjectAsync(_superToken, Content.Project, ReturnFormat.json, projectData, OnErrorFormat.json, null).Result; + var CreateProjectAsyncResult = redcap_api_1_1_0.CreateProjectAsync(_superToken, Content.Project, ReturnFormat.json, projectData, OnErrorFormat.json, null).Result; Console.WriteLine($"CreateProjectAsyncResult: {CreateProjectAsyncResult}"); #endregion CreateProjectAsync() Console.WriteLine("----------------------------Press Enter to Continue-------------"); @@ -502,7 +530,7 @@ static void InitializeDemo() #region ImportProjectInfoAsync() var projectInfo = new RedcapProjectInfo { ProjectTitle = "Updated Amazing Project ", Purpose = ProjectPurpose.QualityImprovement, SurveysEnabled = 1 }; Console.WriteLine($"Calling ImportProjectInfoAsync()"); - var ImportProjectInfoAsyncResult = redcap_api_1_0_7.ImportProjectInfoAsync(_token, Content.ProjectSettings, ReturnFormat.json, projectInfo).Result; + var ImportProjectInfoAsyncResult = redcap_api_1_1_0.ImportProjectInfoAsync(_token, Content.ProjectSettings, ReturnFormat.json, projectInfo).Result; Console.WriteLine($"ImportProjectInfoAsyncResult: {ImportProjectInfoAsyncResult}"); #endregion ImportProjectInfoAsync() Console.WriteLine("----------------------------Press Enter to Continue-------------"); @@ -510,7 +538,7 @@ static void InitializeDemo() #region ExportProjectInfoAsync() Console.WriteLine($"Calling ExportProjectInfoAsync()"); - var ExportProjectInfoAsyncResult = redcap_api_1_0_7.ExportProjectInfoAsync(_token, Content.ProjectSettings, ReturnFormat.json).Result; + var ExportProjectInfoAsyncResult = redcap_api_1_1_0.ExportProjectInfoAsync(_token, Content.ProjectSettings, ReturnFormat.json).Result; Console.WriteLine($"ExportProjectInfoAsyncResult: {ExportProjectInfoAsyncResult}"); #endregion ExportProjectInfoAsync() @@ -522,7 +550,7 @@ static void InitializeDemo() Console.WriteLine($"Using record 1"); Console.WriteLine($"Using instrumentname = demographics"); var instrumentName = new string[] { "demographics" }; - var ExportRecordsAsyncResult = redcap_api_1_0_7.ExportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, null, null, instrumentName).Result; + var ExportRecordsAsyncResult = redcap_api_1_1_0.ExportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, null, null, instrumentName).Result; Console.WriteLine($"ExportRecordsAsyncResult: {ExportProjectInfoAsyncResult}"); #endregion ExportRecordsAsync() From 3211c645fd154725ba4e4beffc9ca176863af176 Mon Sep 17 00:00:00 2001 From: Michael Tran Date: Fri, 5 Feb 2021 11:54:27 -0500 Subject: [PATCH 12/13] add model to support dag update demo add dag type --- RedcapApi/Api/Redcap.cs | 15 +-- RedcapApi/Models/RedcapDag.cs | 32 ++++++ RedcapApiDemo/Program.cs | 171 +++++++++++++++++++++-------- RedcapApiDemo/RedcapApiDemo.csproj | 1 + RedcapApiDemo/SampleData.cs | 10 -- 5 files changed, 167 insertions(+), 62 deletions(-) create mode 100644 RedcapApi/Models/RedcapDag.cs delete mode 100644 RedcapApiDemo/SampleData.cs diff --git a/RedcapApi/Api/Redcap.cs b/RedcapApi/Api/Redcap.cs index 9a7ef5a..da0b2c8 100644 --- a/RedcapApi/Api/Redcap.cs +++ b/RedcapApi/Api/Redcap.cs @@ -195,7 +195,8 @@ public async Task ExportDagsAsync(string token, Content content, ReturnF /// POST /// Import DAGs /// This method allows you to import new DAGs (Data Access Groups) into a project or update the group name of any existing DAGs. - /// NOTE: DAGs can be renamed by simply changing the group name(data_access_group_name). DAG can be created by providing group name value while unique group name should be set to blank. + /// NOTE: DAGs can be renamed by simply changing the group name(data_access_group_name). + /// DAG can be created by providing group name value while unique group name should be set to blank. /// /// /// To use this method, you must have API Import/Update privileges in the project. @@ -237,7 +238,8 @@ public async Task ImportDagsAsync(string token, Content content, Redc { "data", _serializedData } }; // Execute request - return await this.SendPostRequestAsync(payload, _uri); + importDagsResults = await this.SendPostRequestAsync(payload, _uri); + return importDagsResults; } catch (Exception Ex) { @@ -3275,6 +3277,7 @@ public async Task ImportRecordsAsync(string token, ReturnFormat forma /// the content specified by returnContent public async Task ImportRecordsAsync(string token, Content content, ReturnFormat format, RedcapDataType redcapDataType, OverwriteBehavior overwriteBehavior, bool forceAutoNumber, List data, string dateFormat = "", CsvDelimiter csvDelimiter = CsvDelimiter.tab, ReturnContent returnContent = ReturnContent.count, OnErrorFormat onErrorFormat = OnErrorFormat.json) { + var importRecordsResults = string.Empty; try { this.CheckToken(token); @@ -3301,15 +3304,13 @@ public async Task ImportRecordsAsync(string token, Content content, R { payload.Add("returnContent", returnContent.ToString()); } - return await this.SendPostRequestAsync(payload, _uri); + importRecordsResults = await this.SendPostRequestAsync(payload, _uri); + return importRecordsResults; } catch (Exception Ex) { - /* - * We'll just log the error and return the error message. - */ Log.Error($"{Ex.Message}"); - return Ex.Message; + return importRecordsResults; } } diff --git a/RedcapApi/Models/RedcapDag.cs b/RedcapApi/Models/RedcapDag.cs new file mode 100644 index 0000000..13d17d2 --- /dev/null +++ b/RedcapApi/Models/RedcapDag.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Text; + +using Newtonsoft.Json; + +namespace Redcap.Models +{ + /// + /// Data Access Group + /// Example: + /// [{"data_access_group_name":"CA Site","unique_group_name":"ca_site"} + /// {"data_access_group_name":"FL Site","unique_group_name":"fl_site"}, + /// { "data_access_group_name":"New Site","unique_group_name":""}] + /// + public class RedcapDag + { + /// + /// group name + /// + /// + [JsonProperty("data_access_group_name")] + public string GroupName { get; set; } + /// + /// auto-generated unique group name + /// + /// + [JsonProperty("unique_group_name")] + + public string UniqueGroupName { get; set; } + } +} diff --git a/RedcapApiDemo/Program.cs b/RedcapApiDemo/Program.cs index ac51c7f..17eec2f 100644 --- a/RedcapApiDemo/Program.cs +++ b/RedcapApiDemo/Program.cs @@ -1,12 +1,17 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using System.Linq; +using System.Reflection; using Newtonsoft.Json; using Redcap; using Redcap.Models; +using Tynamix.ObjectFiller; + namespace RedcapApiDemo { /// @@ -26,6 +31,12 @@ public class Demographic public string LastName { get; set; } [JsonProperty("bio")] public string Bio { get; set; } + + /// + /// Test file uploads + /// + [JsonProperty("upload_file")] + public string UploadFile { get; set; } } class Program { @@ -194,8 +205,6 @@ public static void Main(string[] args) //string importFileName = "test.txt"; //var pathExport = "C:\\redcap_download_files"; //var record = "1"; - var fieldName = "protocol_upload"; - var eventName = "event_1_arm_1"; //var repeatingInstrument = "1"; //Console.WriteLine("Calling ImportFile() . . ."); @@ -235,7 +244,7 @@ static void InitializeDemo() * Using a local redcap development instsance */ string _uri = string.Empty; - var fieldName = "protocol_upload"; + var fieldName = "file_upload"; var eventName = "event_1_arm_1"; /* @@ -253,10 +262,10 @@ static void InitializeDemo() _uri = _uri + "/api/"; Console.WriteLine("Enter your api token for the project to test: "); var token = Console.ReadLine(); - + if (string.IsNullOrEmpty(token)) { - _token = "93DC9C0B48C9F7C26DF67B849B1A4124"; + _token = "DF70F2EC94AE05021F66423B386095BD"; } else { @@ -284,36 +293,70 @@ static void InitializeDemo() #endregion + #region ImportDagsAsync() + Console.WriteLine("Calling ImportDagsAsync() . . ."); + Console.WriteLine($"Importing Dags . . ."); + var dags = CreateDags(5); + var ImportDagsAsyncResult = redcap_api_1_1_0.ImportDagsAsync(_token, Content.Dag, RedcapAction.Import, ReturnFormat.json, dags).Result; + Console.WriteLine($"ImportDagsAsync Results: {JsonConvert.DeserializeObject(ImportDagsAsyncResult)}"); + Console.WriteLine("----------------------------Press Enter to Continue-------------"); + Console.ReadLine(); + + #endregion + + #region ExportDagsAsync() + Console.WriteLine("Calling ExportDagsAsync() . . ."); + Console.WriteLine($"Exporting Dags . . ."); + var ExportDagsAsyncResult = redcap_api_1_1_0.ExportDagsAsync(_token, Content.Dag, ReturnFormat.json).Result; + Console.WriteLine($"ExportDagsAsync Results: {JsonConvert.DeserializeObject(ExportDagsAsyncResult)}"); + Console.WriteLine("----------------------------Press Enter to Continue-------------"); + Console.ReadLine(); + #endregion + + #region DeleteDagsAsync() + Console.WriteLine("Calling DeleteDagsAsync() . . ."); + Console.WriteLine($"Deleting Dags . . ."); + var dagsToDelete = JsonConvert.DeserializeObject>(ExportDagsAsyncResult).Select(x => x.UniqueGroupName).ToArray(); + var DeleteDagsAsyncResult = redcap_api_1_1_0.DeleteDagsAsync(_token, Content.Dag, RedcapAction.Delete, dagsToDelete).Result; + Console.WriteLine($"DeleteDagsAsync Results: {JsonConvert.DeserializeObject(DeleteDagsAsyncResult)}"); + Console.WriteLine("----------------------------Press Enter to Continue-------------"); + Console.ReadLine(); + #endregion + #region ImportRecordsAsync() Console.WriteLine("Calling ImportRecordsAsync() . . ."); - /* - * Create a list of object of type instrument or fields. Add its properties then add it to the list. - * record_id is required - */ - var data = new List { - new Demographic { - FirstName = "Jon", LastName = "Doe", RecordId = "1" - } - }; - Console.WriteLine($"Importing record {string.Join(",", data.Select(x => x.RecordId).ToList())} . . ."); - var ImportRecordsAsync = redcap_api_1_1_0.ImportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, OverwriteBehavior.normal, false, data, "MDY", CsvDelimiter.tab, ReturnContent.count, OnErrorFormat.json).Result; + // get demographics data + var importDemographicsData = CreateDemographics(includeBio: true, 5); + Console.WriteLine("Serializing the data . . ."); + Console.WriteLine($"Importing record {string.Join(",", importDemographicsData.Select(x => x.RecordId).ToList())} . . ."); + var ImportRecordsAsync = redcap_api_1_1_0.ImportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, OverwriteBehavior.normal, false, importDemographicsData, "MDY", CsvDelimiter.tab, ReturnContent.count, OnErrorFormat.json).Result; var ImportRecordsAsyncData = JsonConvert.DeserializeObject(ImportRecordsAsync); Console.WriteLine($"ImportRecordsAsync Result: {ImportRecordsAsyncData}"); + Console.WriteLine("----------------------------Press Enter to Continue-------------"); + Console.ReadLine(); #endregion ImportRecordsAsync() + #region ExportRecordsAsync() + Console.WriteLine($"Calling ExportRecordsAsync()"); + Console.WriteLine($"Using records from the imported method.."); + var recordsToExport = importDemographicsData.Select(x => x.RecordId).ToArray(); + var instrumentName = new string[] { "demographics" }; + var ExportRecordsAsyncResult = redcap_api_1_1_0.ExportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, recordsToExport, null, instrumentName).Result; + Console.WriteLine($"ExportRecordsAsyncResult: {ExportRecordsAsyncResult}"); Console.WriteLine("----------------------------Press Enter to Continue-------------"); Console.ReadLine(); + #endregion ExportRecordsAsync() #region DeleteRecordsAsync() Console.WriteLine("Calling DeleteRecordsAsync() . . ."); - var records = new string[] { "1" }; - Console.WriteLine($"Deleting record {string.Join(",", records)} . . ."); - var DeleteRecordsAsync = redcap_api_1_1_0.DeleteRecordsAsync(_token, Content.Record, RedcapAction.Delete, records, 1).Result; + var records = importDemographicsData.Select(x => x.RecordId).ToArray(); + Console.WriteLine($"Deleting record {string.Join(",", recordsToExport)} . . ."); + var DeleteRecordsAsync = redcap_api_1_1_0.DeleteRecordsAsync(_token, Content.Record, RedcapAction.Delete, recordsToExport, 1).Result; + var DeleteRecordsAsyncData = JsonConvert.DeserializeObject(DeleteRecordsAsync); + Console.WriteLine($"DeleteRecordsAsync Result: {DeleteRecordsAsyncData}"); Console.WriteLine("----------------------------Press Enter to Continue-------------"); Console.ReadLine(); - - Console.WriteLine($"DeleteRecordsAsync Result: {DeleteRecordsAsync}"); #endregion DeleteRecordsAsync() @@ -321,27 +364,27 @@ static void InitializeDemo() var arms = new string[] { }; Console.WriteLine("Calling ExportArmsAsync()"); var ExportArmsAsyncResult = redcap_api_1_1_0.ExportArmsAsync(_token, Content.Arm, ReturnFormat.json, arms, OnErrorFormat.json).Result; - Console.WriteLine($"ExportArmsAsyncResult: {ExportArmsAsyncResult}"); + Console.WriteLine($"ExportArmsAsyncResult: {JsonConvert.DeserializeObject(ExportArmsAsyncResult)}"); #endregion ExportArmsAsync() Console.WriteLine("----------------------------Press Enter to Continue-------------"); Console.ReadLine(); #region ImportArmsAsync() - var ImportArmsAsyncData = new List { new RedcapArm { ArmNumber = "1", Name = "hooo" }, new RedcapArm { ArmNumber = "2", Name = "heee" }, new RedcapArm { ArmNumber = "3", Name = "hawww" } }; + var ImportArmsAsyncData = CreateArms(count: 3); Console.WriteLine("Calling ImportArmsAsync()"); var ImportArmsAsyncResult = redcap_api_1_1_0.ImportArmsAsync(_token, Content.Arm, Override.False, RedcapAction.Import, ReturnFormat.json, ImportArmsAsyncData, OnErrorFormat.json).Result; - Console.WriteLine($"ImportArmsAsyncResult: {ImportArmsAsyncResult}"); + Console.WriteLine($"ImportArmsAsyncResult: {JsonConvert.DeserializeObject(ImportArmsAsyncResult)}"); #endregion ImportArmsAsync() Console.WriteLine("----------------------------Press Enter to Continue-------------"); Console.ReadLine(); #region DeleteArmsAsync() - var DeleteArmsAsyncData = new string[] { "3" }; + var DeleteArmsAsyncData = ImportArmsAsyncData.Select(x => x.ArmNumber).ToArray(); Console.WriteLine("Calling DeleteArmsAsync()"); var DeleteArmsAsyncResult = redcap_api_1_1_0.DeleteArmsAsync(_token, Content.Arm, RedcapAction.Delete, DeleteArmsAsyncData).Result; - Console.WriteLine($"DeleteArmsAsyncResult: {DeleteArmsAsyncResult}"); + Console.WriteLine($"DeleteArmsAsyncResult: {JsonConvert.DeserializeObject(DeleteArmsAsyncResult)}"); #endregion DeleteArmsAsync() Console.WriteLine("----------------------------Press Enter to Continue-------------"); @@ -351,7 +394,7 @@ static void InitializeDemo() var ExportEventsAsyncData = new string[] { "1" }; Console.WriteLine("Calling ExportEventsAsync()"); var ExportEventsAsyncResult = redcap_api_1_1_0.ExportEventsAsync(_token, Content.Event, ReturnFormat.json, ExportEventsAsyncData, OnErrorFormat.json).Result; - Console.WriteLine($"ExportEventsAsyncResult: {ExportEventsAsyncResult}"); + Console.WriteLine($"ExportEventsAsyncResult: {JsonConvert.DeserializeObject(ExportEventsAsyncResult)}"); #endregion ExportEventsAsync() Console.WriteLine("----------------------------Press Enter to Continue-------------"); @@ -410,10 +453,13 @@ static void InitializeDemo() #region ImportFileAsync() var recordId = "1"; - var fileName = "test.txt"; - var fileUploadPath = @"C:\redcap_upload_files"; + var fileName = "Demographics_TestProject_DataDictionary.csv"; + DirectoryInfo myDirectory = new DirectoryInfo(Environment.CurrentDirectory); + string parentDirectory = myDirectory.Parent.FullName; + var parent = Directory.GetParent(parentDirectory).FullName; + var filePath = Directory.GetParent(parent).FullName + @"\Docs\"; Console.WriteLine($"Calling ImportFileAsync(), {fileName}"); - var ImportFileAsyncResult = redcap_api_1_1_0.ImportFileAsync(_token, Content.File, RedcapAction.Import, recordId, fieldName, eventName, null, fileName, fileUploadPath, OnErrorFormat.json).Result; + var ImportFileAsyncResult = redcap_api_1_1_0.ImportFileAsync(_token, Content.File, RedcapAction.Import, recordId, fieldName, eventName, null, fileName, filePath, OnErrorFormat.json).Result; Console.WriteLine($"ImportFileAsyncResult: {ImportFileAsyncResult}"); #endregion ImportFileAsync() @@ -476,11 +522,12 @@ static void InitializeDemo() Console.WriteLine("----------------------------Press Enter to Continue-------------"); Console.ReadLine(); - #region ExportInstrumentMappingAsync() - Console.WriteLine($"Calling ExportInstrumentMappingAsync()"); - var ExportInstrumentMappingAsyncResult = redcap_api_1_1_0.ExportInstrumentMappingAsync(_token, Content.FormEventMapping, ReturnFormat.json, arms, OnErrorFormat.json).Result; - Console.WriteLine($"ExportInstrumentMappingAsyncResult: {ExportInstrumentMappingAsyncResult}"); - #endregion ExportInstrumentMappingAsync() + //#region ExportInstrumentMappingAsync() + //Console.WriteLine($"Calling ExportInstrumentMappingAsync()"); + //var armsToExportInstrumentMapping = arms; + //var ExportInstrumentMappingAsyncResult = redcap_api_1_1_0.ExportInstrumentMappingAsync(_token, Content.FormEventMapping, ReturnFormat.json, armsToExportInstrumentMapping, OnErrorFormat.json).Result; + //Console.WriteLine($"ExportInstrumentMappingAsyncResult: {ExportInstrumentMappingAsyncResult}"); + //#endregion ExportInstrumentMappingAsync() Console.WriteLine("----------------------------Press Enter to Continue-------------"); Console.ReadLine(); @@ -545,17 +592,8 @@ static void InitializeDemo() Console.WriteLine("----------------------------Demo completed! Press Enter to Exit-------------"); Console.ReadLine(); - #region ExportRecordsAsync() - Console.WriteLine($"Calling ExportRecordsAsync()"); - Console.WriteLine($"Using record 1"); - Console.WriteLine($"Using instrumentname = demographics"); - var instrumentName = new string[] { "demographics" }; - var ExportRecordsAsyncResult = redcap_api_1_1_0.ExportRecordsAsync(_token, Content.Record, ReturnFormat.json, RedcapDataType.flat, null, null, instrumentName).Result; - Console.WriteLine($"ExportRecordsAsyncResult: {ExportProjectInfoAsyncResult}"); - #endregion ExportRecordsAsync() - Console.WriteLine("----------------------------Demo completed! Press Enter to Exit-------------"); - Console.ReadLine(); + } public static Demographic GetRandomPerson(string id, bool includeBio = false) @@ -570,6 +608,49 @@ public static Demographic GetRandomPerson(string id, bool includeBio = false) } return person; } + public static List CreateDemographics(bool includeBio = false, int count = 1) + { + var demographics = new List(); + for (var i = 1; i <= count; i++) + { + var _demographicFiller = new Filler(); + + _demographicFiller.Setup().OnProperty(x => x.RecordId).Use(i.ToString()); + var _demographic = _demographicFiller.Create(); + if (includeBio) + { + _demographic.Bio = VeryLargeText; + } + demographics.Add(_demographic); + } + + return demographics; + } + public static List CreateArms(int count = 1) + { + + var arms = new List(); + for (var i = 0; i < count; i++) + { + var _demographicFiller = new Filler(); + _demographicFiller.Setup().OnProperty(x => x.ArmNumber).Use(i.ToString()); + var _demographic = _demographicFiller.Create(); + arms.Add(_demographic); + } + return arms; + } + public static List CreateDags(int count = 1){ + + var dags = new List(); + for(var i = 0; i < count; i++) + { + var _dagFiller = new Filler(); + _dagFiller.Setup().OnProperty(x => x.UniqueGroupName).Use(string.Empty); + var _dag = _dagFiller.Create(); + dags.Add(_dag); + } + return dags; + } } diff --git a/RedcapApiDemo/RedcapApiDemo.csproj b/RedcapApiDemo/RedcapApiDemo.csproj index 38c7769..5c7cc2a 100644 --- a/RedcapApiDemo/RedcapApiDemo.csproj +++ b/RedcapApiDemo/RedcapApiDemo.csproj @@ -9,6 +9,7 @@ + diff --git a/RedcapApiDemo/SampleData.cs b/RedcapApiDemo/SampleData.cs deleted file mode 100644 index a6bc46a..0000000 --- a/RedcapApiDemo/SampleData.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace RedcapApiDemo -{ - class SampleData - { - } -} From a8211ea3a5e5ea5bd2f23d31687198a7179c90d9 Mon Sep 17 00:00:00 2001 From: Michael Tran Date: Fri, 5 Feb 2021 12:19:25 -0500 Subject: [PATCH 13/13] update travis build configuration --- .travis.yml | 2 +- RedcapApi/Redcap.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 14242d1..c47ddb4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: csharp mono: none -dotnet: 2.1.810 +dotnet: 5.0.2 script: - dotnet restore - dotnet build ./RedcapApi/ diff --git a/RedcapApi/Redcap.csproj b/RedcapApi/Redcap.csproj index f4cd799..16d6ffc 100644 --- a/RedcapApi/Redcap.csproj +++ b/RedcapApi/Redcap.csproj @@ -20,8 +20,8 @@ -DeleteDagsAsync -ExportUserDagAssignmentAsync -ImportUserDagAssignmentAsync -Minor bugs fixes to existing APIs regarding optional parameters. -Version bump +-Added models to support new APIs +Minor bugs fixes to existing APIs regarding optional parameters. false Library