Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into adapt-onboarding
Browse files Browse the repository at this point in the history
  • Loading branch information
tamslo committed Mar 6, 2023
2 parents 7694176 + 3e9d649 commit 99318d7
Show file tree
Hide file tree
Showing 29 changed files with 419 additions and 149 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ defaults:
env:
JAVA_VERSION: 12.x
FLUTTER_CHANNEL: stable
FLUTTER_VERSION: 3.0.3
FLUTTER_VERSION: 3.3.4

jobs:
lint:
Expand All @@ -24,7 +24,7 @@ jobs:
- uses: actions/setup-java@v1
with:
java-version: ${{ env.JAVA_VERSION }}
- uses: subosito/flutter-action@v1
- uses: subosito/flutter-action@v2
with:
channel: ${{ env.FLUTTER_CHANNEL }}
flutter-version: ${{ env.FLUTTER_VERSION }}
Expand Down
7 changes: 5 additions & 2 deletions anni/src/components/bricks/BrickForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const BrickForm = ({ usage, brick }: Props) => {
setMessage(`Failed to ${id ? 'update' : 'add new'} Brick.`);
}
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const deleteBrick = async () => {
try {
await axios.delete(`/api/bricks/${id}`);
Expand Down Expand Up @@ -144,7 +145,9 @@ const BrickForm = ({ usage, brick }: Props) => {
Cancel
</WithIcon>
<div className="space-x-4">
{id && (
{/*
// Disabled for #378 until proper handling of deletion
id && (
<WithIcon
as="button"
icon={TrashIcon}
Expand All @@ -153,7 +156,7 @@ const BrickForm = ({ usage, brick }: Props) => {
>
Delete Brick
</WithIcon>
)}
)*/}
<WithIcon
as="button"
icon={UploadIcon}
Expand Down
16 changes: 12 additions & 4 deletions app/integration_test/drugs_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,15 @@ void main() {
geneSymbol: 'CYP2C9',
phenotype: 'Normal Metabolizer',
genotype: '*1/*1',
lookupkey: 'Normal Metabolizer')
lookupkey: '2')
};
UserData.instance.diplotypes = {
'CYP2C9': Diplotype(
gene: 'CYP2C9',
resultType: 'Diplotype',
phenotype: 'Normal Metabolizer',
genotype: '*1/*1',
allelesTested: '1')
};
final testDrugWithoutGuidelines = Drug(
id: '2',
Expand All @@ -63,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 @@ -116,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 @@ -180,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
4 changes: 2 additions & 2 deletions 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;
FilterState get filter => FilterState.initial();
}

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

testWidgets('test search page in loaded state', (tester) async {
when(() => mockSearchCubit.state)
.thenReturn(SearchState.loaded(loadedDrugs, loadedDrugs));
.thenReturn(SearchState.loaded(loadedDrugs, FilterState.initial()));

await tester.pumpWidget(BlocProvider.value(
value: mockSearchCubit,
Expand Down
2 changes: 1 addition & 1 deletion app/integration_test/settings_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ void main() {
);

// close dialog
await tester.tap(find.text(context.l10n.settings_page_cancel));
await tester.tap(find.text(context.l10n.action_cancel));
await tester.pumpAndSettle();

// test onboarding button
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',
}
};
4 changes: 3 additions & 1 deletion app/lib/common/models/drug/warning_level.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ enum WarningLevel {
@HiveField(1)
yellow,
@HiveField(2)
green
green,
@HiveField(3)
none,
}

extension WarningLevelIcon on WarningLevel {
Expand Down
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
25 changes: 23 additions & 2 deletions 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 @@ -30,13 +31,33 @@ class UserData {
}

@HiveField(0)
List<Diplotype>? diplotypes;
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;
}

// hive can't deal with sets so we have to use a list :(
@HiveField(2)
List<String>? starredDrugIds;
List<String>? activeDrugNames;
}

/// Initializes the user's data by registering all necessary adapters and
Expand Down
22 changes: 12 additions & 10 deletions app/lib/common/pages/drug/cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,34 @@ 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 {
// ignore: avoid_positional_boolean_parameters
Future<void> setActivity(bool? value) async {
if (value == null) return;
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();
} else {
UserData.instance.starredDrugIds = stars + [_drug.id];
final active = (UserData.instance.activeDrugNames ?? [])
.filter((name) => name != _drug.name)
.toList();
if (value) {
active.add(_drug.name);
}
UserData.instance.activeDrugNames = active;
await UserData.save();
emit(DrugState.loaded(drug, isStarred: drug.isStarred()));
emit(DrugState.loaded(drug, isActive: value));
}
}

@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;
}
23 changes: 12 additions & 11 deletions app/lib/common/pages/drug/drug.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,9 @@ 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),
),
IconButton(
onPressed: () => sharePdf(drug),
icon: Icon(
Expand All @@ -44,7 +40,7 @@ class DrugPage extends StatelessWidget {
)
],
body: [
_buildDrugsPage(drug, isStarred: isStarred, context: context)
_buildDrugsPage(drug, isActive: isActive, context: context)
],
),
);
Expand All @@ -55,28 +51,33 @@ 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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SubHeader(context.l10n.drugs_page_header_druginfo),
SubHeader(context.l10n.drugs_page_header_drug),
SizedBox(height: 12),
DrugAnnotationCard(drug),
DrugAnnotationCard(
drug,
isActive: isActive,
setActivity: context.read<DrugCubit>().setActivity,
),
SizedBox(height: 20),
SubHeader(
context.l10n.drugs_page_header_guideline,
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
Loading

0 comments on commit 99318d7

Please sign in to comment.