generate IPS bundle and composition
adding test when generating summary.
This commit is contained in:
parent
c3184fa8c0
commit
dd78494325
|
@ -67,9 +67,15 @@ func (gr *GormRepository) QueryResources(ctx context.Context, query models.Query
|
|||
return results, clientResp.Error
|
||||
|
||||
} else {
|
||||
results := []models.ResourceBase{}
|
||||
clientResp := sqlQuery.Find(&results)
|
||||
return results, clientResp.Error
|
||||
|
||||
//find the associated Gorm Model for this query
|
||||
queryModelSlice, err := databaseModel.NewFhirResourceModelSliceByType(query.From)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientResp := sqlQuery.Find(&queryModelSlice)
|
||||
return queryModelSlice, clientResp.Error
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,16 +6,23 @@ import (
|
|||
"fmt"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/models/database"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/utils/ips"
|
||||
"github.com/fastenhealth/gofhir-models/fhir401"
|
||||
"github.com/google/uuid"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (gr *GormRepository) GetInternationalPatientSummaryBundle(ctx context.Context) (interface{}, error) {
|
||||
func (gr *GormRepository) GetInternationalPatientSummaryBundle(ctx context.Context) (interface{}, interface{}, error) {
|
||||
summaryTime := time.Now()
|
||||
timestamp := summaryTime.Format(time.RFC3339)
|
||||
|
||||
narrativeEngine, err := ips.NewNarrative()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error creating narrative engine: %w", err)
|
||||
}
|
||||
|
||||
//Algorithm to create the IPS bundle
|
||||
// 1. Generate the IPS Section Lists (GetInternationalPatientSummarySectionResources)
|
||||
// - Process each resource, generating a Markdown narrative in the text field for each resource
|
||||
|
@ -34,15 +41,15 @@ func (gr *GormRepository) GetInternationalPatientSummaryBundle(ctx context.Conte
|
|||
//Step 1. Generate the IPS Section Lists
|
||||
summarySectionResources, err := gr.GetInternationalPatientSummarySectionResources(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
//Step 2. Create the Composition Section
|
||||
compositionSections := []fhir401.CompositionSection{}
|
||||
for sectionType, sectionResources := range summarySectionResources {
|
||||
compositionSection, err := generateIPSCompositionSection(sectionType, sectionResources)
|
||||
compositionSection, err := generateIPSCompositionSection(narrativeEngine, sectionType, sectionResources)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
compositionSections = append(compositionSections, *compositionSection)
|
||||
}
|
||||
|
@ -115,6 +122,7 @@ func (gr *GormRepository) GetInternationalPatientSummaryBundle(ctx context.Conte
|
|||
},
|
||||
},
|
||||
}
|
||||
ipsComposition.Section = compositionSections
|
||||
|
||||
// Step 6. Create the IPS Bundle
|
||||
bundleUUID := uuid.New().String()
|
||||
|
@ -129,7 +137,7 @@ func (gr *GormRepository) GetInternationalPatientSummaryBundle(ctx context.Conte
|
|||
// Add the Composition to the bundle
|
||||
ipsCompositionJson, err := json.Marshal(ipsComposition)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
ipsBundle.Entry = append(ipsBundle.Entry, fhir401.BundleEntry{
|
||||
Resource: json.RawMessage(ipsCompositionJson),
|
||||
|
@ -142,28 +150,28 @@ func (gr *GormRepository) GetInternationalPatientSummaryBundle(ctx context.Conte
|
|||
for _, sectionResources := range summarySectionResources {
|
||||
for _, resource := range sectionResources {
|
||||
ipsBundle.Entry = append(ipsBundle.Entry, fhir401.BundleEntry{
|
||||
Resource: json.RawMessage(resource.ResourceRaw),
|
||||
Resource: json.RawMessage(resource.GetResourceRaw()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return ipsBundle, nil
|
||||
return ipsBundle, ipsComposition, nil
|
||||
}
|
||||
|
||||
// GetInternationalPatientSummary will generate an IPS bundle, which can then be used to generate a IPS QR code, PDF or JSON bundle
|
||||
// The IPS bundle will contain a summary of all the data in the system, including a list of all sources, and the main Patient
|
||||
// See: https://github.com/fastenhealth/fasten-onprem/issues/170
|
||||
// See: https://github.com/jddamore/fhir-ips-server/blob/main/docs/Summary_Creation_Steps.md
|
||||
func (gr *GormRepository) GetInternationalPatientSummarySectionResources(ctx context.Context) (map[pkg.IPSSections][]models.ResourceBase, error) {
|
||||
func (gr *GormRepository) GetInternationalPatientSummarySectionResources(ctx context.Context) (map[pkg.IPSSections][]database.IFhirResourceModel, error) {
|
||||
|
||||
summarySectionResources := map[pkg.IPSSections][]models.ResourceBase{}
|
||||
summarySectionResources := map[pkg.IPSSections][]database.IFhirResourceModel{}
|
||||
|
||||
// generate queries for each IPS Section
|
||||
for ndx, _ := range pkg.IPSSectionsList {
|
||||
sectionName := pkg.IPSSectionsList[ndx]
|
||||
|
||||
//initialize the section
|
||||
summarySectionResources[sectionName] = []models.ResourceBase{}
|
||||
summarySectionResources[sectionName] = []database.IFhirResourceModel{}
|
||||
|
||||
queries, err := generateIPSSectionQueries(sectionName)
|
||||
if err != nil {
|
||||
|
@ -176,7 +184,7 @@ func (gr *GormRepository) GetInternationalPatientSummarySectionResources(ctx con
|
|||
return nil, err
|
||||
}
|
||||
|
||||
resultsList := results.([]models.ResourceBase)
|
||||
resultsList := convertUnknownInterfaceToFhirSlice(results)
|
||||
|
||||
//TODO: generate resource narrative
|
||||
summarySectionResources[sectionName] = append(summarySectionResources[sectionName], resultsList...)
|
||||
|
@ -186,7 +194,7 @@ func (gr *GormRepository) GetInternationalPatientSummarySectionResources(ctx con
|
|||
return summarySectionResources, nil
|
||||
}
|
||||
|
||||
func generateIPSCompositionSection(sectionType pkg.IPSSections, resources []models.ResourceBase) (*fhir401.CompositionSection, error) {
|
||||
func generateIPSCompositionSection(narrativeEngine *ips.Narrative, sectionType pkg.IPSSections, resources []database.IFhirResourceModel) (*fhir401.CompositionSection, error) {
|
||||
sectionTitle, sectionCode, err := generateIPSSectionHeaderInfo(sectionType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -210,7 +218,7 @@ func generateIPSCompositionSection(sectionType pkg.IPSSections, resources []mode
|
|||
section.Entry = []fhir401.Reference{}
|
||||
for _, resource := range resources {
|
||||
reference := fhir401.Reference{
|
||||
Reference: stringPtr(fmt.Sprintf("%s/%s", resource.SourceResourceType, resource.SourceID)),
|
||||
Reference: stringPtr(fmt.Sprintf("%s/%s", resource.GetSourceResourceType(), resource.GetSourceResourceID())),
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -219,9 +227,17 @@ func generateIPSCompositionSection(sectionType pkg.IPSSections, resources []mode
|
|||
}
|
||||
|
||||
//TODO: Add the section narrative summary
|
||||
rendered, err := narrativeEngine.RenderSection(
|
||||
sectionType,
|
||||
resources,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error rendering narrative for section %s: %w", sectionType, err)
|
||||
}
|
||||
|
||||
section.Text = &fhir401.Narrative{
|
||||
Status: fhir401.NarrativeStatusGenerated,
|
||||
Div: "PLACEHOLDER NARRATIVE SUMMARY FOR SECTION",
|
||||
Div: rendered,
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -543,6 +559,66 @@ func generateIPSSectionHeaderInfo(sectionType pkg.IPSSections) (string, fhir401.
|
|||
|
||||
}
|
||||
|
||||
// QueryResources returns an interface{} which is actually a slice of the appropriate FHIR resource type
|
||||
// we use this function to "cast" the results to a slice of the IFhirResourceModel interface (so we can use the same code to handle the results)
|
||||
// TODO: there has to be a better way to do this :/
|
||||
func convertUnknownInterfaceToFhirSlice(unknown interface{}) []database.IFhirResourceModel {
|
||||
results := []database.IFhirResourceModel{}
|
||||
|
||||
switch fhirSlice := unknown.(type) {
|
||||
case []database.FhirAllergyIntolerance:
|
||||
for ndx, _ := range fhirSlice {
|
||||
results = append(results, &fhirSlice[ndx])
|
||||
}
|
||||
case []database.FhirCarePlan:
|
||||
for ndx, _ := range fhirSlice {
|
||||
results = append(results, &fhirSlice[ndx])
|
||||
}
|
||||
case []database.FhirCondition:
|
||||
for ndx, _ := range fhirSlice {
|
||||
results = append(results, &fhirSlice[ndx])
|
||||
}
|
||||
case []database.FhirDevice:
|
||||
for ndx, _ := range fhirSlice {
|
||||
results = append(results, &fhirSlice[ndx])
|
||||
}
|
||||
case []database.FhirDiagnosticReport:
|
||||
for ndx, _ := range fhirSlice {
|
||||
results = append(results, &fhirSlice[ndx])
|
||||
}
|
||||
case []database.FhirEncounter:
|
||||
for ndx, _ := range fhirSlice {
|
||||
results = append(results, &fhirSlice[ndx])
|
||||
}
|
||||
case []database.FhirImmunization:
|
||||
for ndx, _ := range fhirSlice {
|
||||
results = append(results, &fhirSlice[ndx])
|
||||
}
|
||||
case []database.FhirMedicationRequest:
|
||||
for ndx, _ := range fhirSlice {
|
||||
results = append(results, &fhirSlice[ndx])
|
||||
}
|
||||
case []database.FhirMedicationStatement:
|
||||
for ndx, _ := range fhirSlice {
|
||||
results = append(results, &fhirSlice[ndx])
|
||||
}
|
||||
case []database.FhirObservation:
|
||||
for ndx, _ := range fhirSlice {
|
||||
results = append(results, &fhirSlice[ndx])
|
||||
}
|
||||
case []database.FhirPatient:
|
||||
for ndx, _ := range fhirSlice {
|
||||
results = append(results, &fhirSlice[ndx])
|
||||
}
|
||||
case []database.FhirProcedure:
|
||||
for ndx, _ := range fhirSlice {
|
||||
results = append(results, &fhirSlice[ndx])
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func generateIPSSectionNarrative(sectionType pkg.IPSSections, resources []models.ResourceBase) string {
|
||||
return ""
|
||||
}
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg"
|
||||
mock_config "github.com/fastenhealth/fasten-onprem/backend/pkg/config/mock"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
|
||||
sourceFactory "github.com/fastenhealth/fasten-sources/clients/factory"
|
||||
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
|
||||
"github.com/fastenhealth/gofhir-models/fhir401"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Define the suite, and absorb the built-in basic suite
|
||||
// functionality from testify - including a T() method which
|
||||
// returns the current testing context
|
||||
type RepositorySummaryTestSuite struct {
|
||||
suite.Suite
|
||||
MockCtrl *gomock.Controller
|
||||
TestDatabase *os.File
|
||||
}
|
||||
|
||||
// BeforeTest has a function to be executed right before the test starts and receives the suite and test names as input
|
||||
func (suite *RepositorySummaryTestSuite) BeforeTest(suiteName, testName string) {
|
||||
suite.MockCtrl = gomock.NewController(suite.T())
|
||||
|
||||
dbFile, err := ioutil.TempFile("", fmt.Sprintf("%s.*.db", testName))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
suite.TestDatabase = dbFile
|
||||
|
||||
}
|
||||
|
||||
// AfterTest has a function to be executed right after the test finishes and receives the suite and test names as input
|
||||
func (suite *RepositorySummaryTestSuite) AfterTest(suiteName, testName string) {
|
||||
suite.MockCtrl.Finish()
|
||||
os.Remove(suite.TestDatabase.Name())
|
||||
os.Remove(suite.TestDatabase.Name() + "-shm")
|
||||
os.Remove(suite.TestDatabase.Name() + "-wal")
|
||||
}
|
||||
|
||||
// In order for 'go test' to run this suite, we need to create
|
||||
// a normal test function and pass our suite to suite.Run
|
||||
func TestRepositorySummaryTestSuiteSuite(t *testing.T) {
|
||||
suite.Run(t, new(RepositorySummaryTestSuite))
|
||||
}
|
||||
|
||||
func (suite *RepositorySummaryTestSuite) TestGetInternationalPatientSummaryBundle() {
|
||||
//setup
|
||||
fakeConfig := mock_config.NewMockInterface(suite.MockCtrl)
|
||||
fakeConfig.EXPECT().GetString("database.location").Return(suite.TestDatabase.Name()).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("database.type").Return("sqlite").AnyTimes()
|
||||
fakeConfig.EXPECT().IsSet("database.encryption.key").Return(false).AnyTimes()
|
||||
fakeConfig.EXPECT().GetString("log.level").Return("INFO").AnyTimes()
|
||||
dbRepo, err := NewRepository(fakeConfig, logrus.WithField("test", suite.T().Name()), event_bus.NewNoopEventBusServer())
|
||||
require.NoError(suite.T(), err)
|
||||
|
||||
userModel := &models.User{
|
||||
Username: "test_username",
|
||||
Password: "testpassword",
|
||||
Email: "test@test.com",
|
||||
}
|
||||
err = dbRepo.CreateUser(context.Background(), userModel)
|
||||
require.NoError(suite.T(), err)
|
||||
require.NotEmpty(suite.T(), userModel.ID)
|
||||
require.NotEqual(suite.T(), uuid.Nil, userModel.ID)
|
||||
authContext := context.WithValue(context.Background(), pkg.ContextKeyTypeAuthUsername, "test_username")
|
||||
|
||||
testSourceCredential := models.SourceCredential{
|
||||
ModelBase: models.ModelBase{
|
||||
ID: uuid.New(),
|
||||
},
|
||||
UserID: userModel.ID,
|
||||
Patient: uuid.New().String(),
|
||||
PlatformType: sourcePkg.PlatformTypeManual,
|
||||
}
|
||||
err = dbRepo.CreateSource(authContext, &testSourceCredential)
|
||||
require.NoError(suite.T(), err)
|
||||
|
||||
bundleFile, err := os.Open("./testdata/Abraham100_Heller342_262b819a-5193-404a-9787-b7f599358035.json")
|
||||
require.NoError(suite.T(), err)
|
||||
|
||||
testLogger := logrus.WithFields(logrus.Fields{
|
||||
"type": "test",
|
||||
})
|
||||
|
||||
manualClient, err := sourceFactory.GetSourceClient(sourcePkg.FastenLighthouseEnvSandbox, authContext, testLogger, &testSourceCredential)
|
||||
|
||||
summary, err := manualClient.SyncAllBundle(dbRepo, bundleFile, sourcePkg.FhirVersion401)
|
||||
require.NoError(suite.T(), err)
|
||||
require.Equal(suite.T(), 198, summary.TotalResources)
|
||||
require.Equal(suite.T(), 234, len(summary.UpdatedResources))
|
||||
|
||||
//test
|
||||
bundle, composition, err := dbRepo.GetInternationalPatientSummaryBundle(authContext)
|
||||
require.NoError(suite.T(), err)
|
||||
|
||||
//case bundle and composition
|
||||
fhirBundle := bundle.(*fhir401.Bundle)
|
||||
fhirComposition := composition.(*fhir401.Composition)
|
||||
|
||||
require.NotNil(suite.T(), fhirBundle)
|
||||
require.NotNil(suite.T(), fhirComposition)
|
||||
|
||||
require.Equal(suite.T(), 211, len(fhirBundle.Entry))
|
||||
require.Equal(suite.T(), 14, len(fhirComposition.Section))
|
||||
|
||||
//require.Equal(suite.T(), "", fhirComposition.Section[0].Title)
|
||||
//require.Equal(suite.T(), "", fhirComposition.Section[0].Text.Div)
|
||||
|
||||
}
|
|
@ -21,7 +21,7 @@ type DatabaseRepository interface {
|
|||
|
||||
//get a count of every resource type
|
||||
GetSummary(ctx context.Context) (*models.Summary, error)
|
||||
GetInternationalPatientSummaryBundle(ctx context.Context) (interface{}, error)
|
||||
GetInternationalPatientSummaryBundle(ctx context.Context) (interface{}, interface{}, error)
|
||||
|
||||
GetResourceByResourceTypeAndId(context.Context, string, string) (*models.ResourceBase, error)
|
||||
GetResourceBySourceId(context.Context, string, string) (*models.ResourceBase, error)
|
||||
|
|
|
@ -33,6 +33,9 @@ func (s *ResourceBase) SetSortDate(sortDate *time.Time) {
|
|||
func (s *ResourceBase) SetResourceRaw(resourceRaw datatypes.JSON) {
|
||||
s.ResourceRaw = resourceRaw
|
||||
}
|
||||
func (s *ResourceBase) GetResourceRaw() datatypes.JSON {
|
||||
return s.ResourceRaw
|
||||
}
|
||||
|
||||
func (s *ResourceBase) SetSourceUri(sourceUri *string) {
|
||||
s.SourceUri = sourceUri
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/database"
|
||||
"github.com/fastenhealth/fasten-onprem/backend/pkg/utils/ips"
|
||||
"github.com/fastenhealth/gofhir-models/fhir401"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
|
@ -25,11 +28,46 @@ func GetIPSSummary(c *gin.Context) {
|
|||
logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry)
|
||||
databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)
|
||||
|
||||
summary, err := databaseRepo.GetInternationalPatientSummaryBundle(c)
|
||||
ipsBundle, ipsComposititon, err := databaseRepo.GetInternationalPatientSummaryBundle(c)
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while retrieving IPS summary", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": summary})
|
||||
|
||||
if c.Query("format") == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": ipsBundle})
|
||||
return
|
||||
}
|
||||
|
||||
narrative, err := ips.NewNarrative()
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while parsing template", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
|
||||
composititon := ipsComposititon.(*fhir401.Composition)
|
||||
|
||||
if c.Query("format") == "html" {
|
||||
|
||||
logger.Debugln("Rendering HTML")
|
||||
//create string writer
|
||||
content, err := narrative.RenderTemplate("index.gohtml", ips.NarrativeTemplateData{Composition: composititon})
|
||||
if err != nil {
|
||||
logger.Errorln("An error occurred while executing template", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
c.String(http.StatusOK, content)
|
||||
return
|
||||
}
|
||||
//} else if c.Query("format") == "pdf" {
|
||||
//
|
||||
// c.Header("Content-Disposition", "attachment; filename=ips_summary.pdf")
|
||||
// c.Header("Content-Type", "application/pdf")
|
||||
// c.String(http.StatusOK, b.String())
|
||||
// return
|
||||
//}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue