Skip to content

Commit

Permalink
Merge pull request #573 from hpi-dhc/issue/443-include-drug-drug-and-…
Browse files Browse the repository at this point in the history
…drug-drug-gene-interaction-checking

Include drug-drug and drug-drug-gene interaction checking
  • Loading branch information
jannis-baum authored Feb 18, 2023
2 parents b0bf4c0 + 001922a commit db57fdc
Show file tree
Hide file tree
Showing 18 changed files with 123 additions and 91 deletions.
8 changes: 4 additions & 4 deletions app/integration_test/drugs_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ void main() {
geneSymbol: 'CYP2C9',
phenotype: 'Normal Metabolizer',
genotype: '*1/*1',
lookupkey: 'Normal Metabolizer')
lookupkey: '2')
};
UserData.instance.diplotypes = {
'CYP2C9': Diplotype(
Expand All @@ -71,7 +71,7 @@ void main() {
brandNames: ['brand name', 'another brand name']),
guidelines: [],
);
UserData.instance.starredDrugIds = ['1'];
UserData.instance.activeDrugNames = ['Ibuprofen'];

group('integration test for the drugs page', () {
testWidgets('test loading', (tester) async {
Expand Down Expand Up @@ -124,7 +124,7 @@ void main() {

testWidgets('test loaded page', (tester) async {
when(() => mockDrugsCubit.state)
.thenReturn(DrugState.loaded(testDrug, isStarred: false));
.thenReturn(DrugState.loaded(testDrug, isActive: false));

late BuildContext context;

Expand Down Expand Up @@ -188,7 +188,7 @@ void main() {

testWidgets('test loaded page without guidelines', (tester) async {
when(() => mockDrugsCubit.state).thenReturn(
DrugState.loaded(testDrugWithoutGuidelines, isStarred: true));
DrugState.loaded(testDrugWithoutGuidelines, isActive: true));

await tester.pumpWidget(
MaterialApp(
Expand Down
2 changes: 1 addition & 1 deletion app/integration_test/search_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import 'package:mocktail/mocktail.dart';

class MockSearchCubit extends MockCubit<SearchState> implements SearchCubit {
@override
bool get filterStarred => false;
bool get filterActive => false;
}

void main() {
Expand Down
52 changes: 10 additions & 42 deletions app/lib/common/models/drug/drug.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ class DrugAnnotations {
List<String> brandNames;
}

extension DrugIsStarred on Drug {
bool isStarred() {
return UserData.instance.starredDrugIds?.contains(id) ?? false;
extension DrugIsActive on Drug {
bool isActive() {
return UserData.instance.activeDrugNames?.contains(name) ?? false;
}
}

Expand All @@ -80,52 +80,20 @@ extension DrugMatchesQuery on Drug {
}
}

/// Removes the guidelines that are not relevant to the user
extension DrugWithUserGuidelines on Drug {
Drug filterUserGuidelines() {
final matchingGuidelines = guidelines.where((guideline) {
// Guideline matches if all user has any of the gene results for all gene
// symbols
return guideline.lookupkey.all((geneSymbol, geneResults) =>
(UserData.instance.lookups?.containsKey(geneSymbol) ?? false) &&
geneResults
.contains(UserData.instance.lookups?[geneSymbol]?.lookupkey));
});

return Drug(
id: id,
version: version,
name: name,
rxNorm: rxNorm,
annotations: annotations,
guidelines: matchingGuidelines.toList(),
);
}
}

/// Removes the guidelines that are not relevant to the user
extension DrugsWithUserGuidelines on List<Drug> {
List<Drug> filterUserGuidelines() {
return map((drug) => drug.filterUserGuidelines()).toList();
}
/// Gets the User's matching guideline
extension DrugWithUserGuideline on Drug {
Guideline? userGuideline() => guidelines.firstOrNullWhere(
(guideline) => guideline.lookupkey.all((geneSymbol, geneResults) =>
geneResults.contains(UserData.lookupFor(geneSymbol))),
);
}

/// Filters for drugs with non-OK warning level
extension CriticalDrugs on List<Drug> {
List<Drug> filterCritical() {
return filter((drug) {
final warningLevel = drug.highestWarningLevel();
final warningLevel = drug.userGuideline()?.annotations.warningLevel;
return warningLevel != null && warningLevel != WarningLevel.green;
}).toList();
}
}

/// Gets most severe warning level
extension DrugWarningLevel on Drug {
WarningLevel? highestWarningLevel() {
final filtered = filterUserGuidelines();
return filtered.guidelines
.map((guideline) => guideline.annotations.warningLevel)
.maxBy((level) => level.severity);
}
}
24 changes: 24 additions & 0 deletions app/lib/common/models/drug/drug_inhibitors.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Everything has to match literally. The final value is not a phenotype but
// the CPIC lookupkey value. If a user has multiple of the given drugs active,
// the topmost one will be used, i.e. the inhibitors should go from most to
// least severe.

// structure: gene symbol -> drug name -> overwriting lookupkey

const Map<String, Map<String, String>> drugInhibitors = {
'CYP2D6': {
// 0.0 is a lookupkey for a type of poor metabolizer
'bupropion': '0.0',
'fluoxetine': '0.0',
'paroxetine': '0.0',
'quinidine': '0.0',
'terbinafine': '0.0',
// 1.0 is a lookupkey for a type of poor (but less poor than 0.0)
// metabolizer
'abiraterone': '1.0',
'cinacalcet': '1.0',
'duloxetine': '1.0',
'lorcaserin': '1.0',
'mirabegron': '1.0',
}
};
1 change: 1 addition & 0 deletions app/lib/common/models/module.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export 'anni_response.dart';
export 'drug/drug.dart';
export 'drug/drug_inhibitors.dart';
export 'drug/guideline.dart';
export 'drug/warning_level.dart';
export 'metadata.dart';
Expand Down
22 changes: 21 additions & 1 deletion app/lib/common/models/userdata/userdata.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:hive/hive.dart';

import '../../../search/module.dart';
Expand Down Expand Up @@ -31,12 +32,31 @@ class UserData {

@HiveField(0)
Map<String, Diplotype>? diplotypes;
static String? phenotypeFor(String gene) =>
UserData.instance.diplotypes?[gene]?.phenotype;

@HiveField(1)
Map<String, CpicPhenotype>? lookups;

static MapEntry<String, String>? overwrittenLookup(String gene) {
final inhibitors = drugInhibitors[gene];
if (inhibitors == null) return null;
final lookup = inhibitors.entries.firstWhereOrNull((entry) =>
UserData.instance.activeDrugNames?.contains(entry.key) ?? false);
if (lookup == null) return null;
return lookup;
}

static String? lookupFor(String gene) {
final overwrittenLookup = UserData.overwrittenLookup(gene);
if (overwrittenLookup != null) {
return overwrittenLookup.value;
}
return UserData.instance.lookups?[gene]?.lookupkey;
}

@HiveField(2)
List<String>? starredDrugIds;
List<String>? activeDrugNames;
}

/// Initializes the user's data by registering all necessary adapters and
Expand Down
18 changes: 9 additions & 9 deletions app/lib/common/pages/drug/cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,32 @@ part 'cubit.freezed.dart';

class DrugCubit extends Cubit<DrugState> {
DrugCubit(this._drug) : super(DrugState.initial()) {
emit(DrugState.loaded(_drug, isStarred: _drug.isStarred()));
emit(DrugState.loaded(_drug, isActive: _drug.isActive()));
}

final Drug _drug;

Future<void> toggleStarred() async {
Future<void> toggleActive() async {
final drug = state.whenOrNull(loaded: (drug, _) => drug);
if (drug == null) return;

final stars = UserData.instance.starredDrugIds ?? [];
if (drug.isStarred()) {
UserData.instance.starredDrugIds =
stars.filter((element) => element != _drug.id).toList();
final active = UserData.instance.activeDrugNames ?? [];
if (drug.isActive()) {
UserData.instance.activeDrugNames =
active.filter((element) => element != _drug.name).toList();
} else {
UserData.instance.starredDrugIds = stars + [_drug.id];
UserData.instance.activeDrugNames = active + [_drug.name];
}
await UserData.save();
emit(DrugState.loaded(drug, isStarred: drug.isStarred()));
emit(DrugState.loaded(drug, isActive: drug.isActive()));
}
}

@freezed
class DrugState with _$DrugState {
const factory DrugState.initial() = _InitialState;
const factory DrugState.loading() = _LoadingState;
const factory DrugState.loaded(Drug drug, {required bool isStarred}) =
const factory DrugState.loaded(Drug drug, {required bool isActive}) =
_LoadedState;
const factory DrugState.error() = _ErrorState;
}
15 changes: 8 additions & 7 deletions app/lib/common/pages/drug/drug.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ class DrugPage extends StatelessWidget {
body: [errorIndicator(context.l10n.err_generic)]),
loading: () =>
pageScaffold(title: drugName, body: [loadingIndicator()]),
loaded: (drug, isStarred) => pageScaffold(
loaded: (drug, isActive) => pageScaffold(
title: drugName,
actions: [
IconButton(
onPressed: () => context.read<DrugCubit>().toggleStarred(),
icon: PharMeTheme.starIcon(isStarred: isStarred),
onPressed: () => context.read<DrugCubit>().toggleActive(),
icon: PharMeTheme.activeDrugIcon(isActive: isActive),
),
IconButton(
onPressed: () => sharePdf(drug),
Expand All @@ -44,7 +44,7 @@ class DrugPage extends StatelessWidget {
)
],
body: [
_buildDrugsPage(drug, isStarred: isStarred, context: context)
_buildDrugsPage(drug, isActive: isActive, context: context)
],
),
);
Expand All @@ -55,9 +55,10 @@ class DrugPage extends StatelessWidget {

Widget _buildDrugsPage(
Drug drug, {
required bool isStarred,
required bool isActive,
required BuildContext context,
}) {
final userGuideline = drug.userGuideline();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
child: Column(
Expand All @@ -72,11 +73,11 @@ class DrugPage extends StatelessWidget {
tooltip: context.l10n.drugs_page_tooltip_guideline,
),
SizedBox(height: 12),
...(drug.guidelines.isNotEmpty)
...(userGuideline != null)
? [
Disclaimer(),
SizedBox(height: 12),
GuidelineAnnotationCard(drug.guidelines[0])
GuidelineAnnotationCard(userGuideline)
]
: [Text(context.l10n.drugs_page_no_guidelines_for_phenotype)]
],
Expand Down
16 changes: 12 additions & 4 deletions app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ class GuidelineAnnotationCard extends StatelessWidget {
color: guideline.annotations.warningLevel.color,
child: Padding(
padding: EdgeInsets.all(12),
child: Column(children: [
child:
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
Icon(guideline.annotations.warningLevel.icon,
color: PharMeTheme.onSurfaceText),
Expand All @@ -55,15 +56,22 @@ class GuidelineAnnotationCard extends StatelessWidget {
}

Widget _buildHeader(BuildContext context) {
final geneDescriptions = guideline.lookupkey.keys.map((geneSymbol) =>
'$geneSymbol (${UserData.instance.diplotypes![geneSymbol]!.phenotype})');
final geneDescriptions = guideline.lookupkey.keys.map((geneSymbol) {
final overwritingDrug = UserData.overwrittenLookup(geneSymbol)?.key;
final hint = overwritingDrug.isNotNullOrEmpty
? '(${context.l10n.drugs_page_overwritten_phenotype(overwritingDrug!)})'
: '';
final genePhenotype =
'$geneSymbol: ${UserData.phenotypeFor(geneSymbol)!}';
return [genePhenotype, hint].join(' ');
});
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
SubHeader(context.l10n.drugs_page_your_genome),
SizedBox(height: 12),
Text(
geneDescriptions.join(', '),
style: PharMeTheme.textTheme.bodyLarge!,
)
),
]);
}

Expand Down
6 changes: 4 additions & 2 deletions app/lib/common/pages/drug/widgets/disclaimer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import '../../../l10n.dart';
import '../../../theme.dart';

class Disclaimer extends StatelessWidget {
const Disclaimer();
const Disclaimer({this.text});

final String? text;

@override
Widget build(BuildContext context) {
Expand All @@ -24,7 +26,7 @@ class Disclaimer extends StatelessWidget {
SizedBox(width: 8),
Flexible(
child: Text(
context.l10n.drugs_page_disclaimer,
text ?? context.l10n.drugs_page_disclaimer,
style: PharMeTheme.textTheme.labelMedium!.copyWith(
fontWeight: FontWeight.w100,
),
Expand Down
4 changes: 2 additions & 2 deletions app/lib/common/theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ class PharMeTheme {
static const errorColor = Color(0xccf52a2a);
static final borderColor = Colors.black.withOpacity(.2);

static Icon starIcon({required bool isStarred, double? size}) {
return Icon(isStarred ? Icons.star_rounded : Icons.star_border_rounded,
static Icon activeDrugIcon({required bool isActive, double? size}) {
return Icon(isActive ? Icons.star_rounded : Icons.star_border_rounded,
size: size, color: primaryColor);
}
}
Expand Down
4 changes: 1 addition & 3 deletions app/lib/common/utilities/drug_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ Future<void> updateCachedDrugs() async {

final dataResponse = await get(anniUrl('data'));
if (dataResponse.statusCode != 200) throw Exception();
final drugs =
AnniDataResponse.fromJson(jsonDecode(dataResponse.body)).data.drugs;
CachedDrugs.instance.drugs = drugs.filterUserGuidelines();
CachedDrugs.instance.drugs = AnniDataResponse.fromJson(jsonDecode(dataResponse.body)).data.drugs;
await CachedDrugs.save();
}
2 changes: 1 addition & 1 deletion app/lib/common/utilities/pdf_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ List<pw.Widget> _buildGuidelinePart(Guideline guideline) {
title: 'Relevant gene phenotypes: ',
text: guideline.lookupkey.keys
.map((geneSymbol) =>
'$geneSymbol: ${UserData.instance.lookups![geneSymbol]!}')
'$geneSymbol: ${UserData.phenotypeFor(geneSymbol)!}')
.join(', ')),
),
pw.SizedBox(height: 8, width: double.infinity),
Expand Down
12 changes: 11 additions & 1 deletion app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"err_could_not_retrieve_access_token": "An unexpected error occurred while retrieving the access token",
"err_fetch_user_data_failed": "An unexpected error occurred while fetching your genomic data",
"err_generic": "Error!",
"err_no_starred_drugs": "You have no starred drugs! Try disabling the filter by tapping the star icon above.",
"err_no_active_drugs": "You have no active drugs! Try disabling the filter next to the search bar above.",

"faq_page_description": "Here you can find answers to common questions about PGx",
"faq_pharmacogenomics": "Pharmacogenomics",
Expand All @@ -19,6 +19,16 @@
"search_page_tooltip_search": "Search for drugs by their name, brand name or class.",

"drugs_page_disclaimer": "This assessment is ONLY based on your genetics. Important factors like weight, age and pre-existing conditions are not considered.",
"drugs_page_overwritten_phenotype": "adjusted based on you taking {drugName}",
"@drugs_page_overwritten_phenotype": {
"description": "Disclaimer for when the phenotype has been adjusted based on an active drug",
"placeholders": {
"drugName": {
"type": "String",
"example": "bupropion"
}
}
},
"drugs_page_your_genome": "Your genome",
"drugs_page_header_further_info": "Further Information",
"drugs_page_header_synonyms": "Synonyms",
Expand Down
Loading

0 comments on commit db57fdc

Please sign in to comment.