diff --git a/api/graphql/schema.py b/api/graphql/schema.py index ac034ffdf..194a57389 100644 --- a/api/graphql/schema.py +++ b/api/graphql/schema.py @@ -54,6 +54,7 @@ ) from models.models.analysis_runner import AnalysisRunnerInternal from models.models.family import PedRowInternal +from models.models.ourdna import OurDNADashboard, OurDNALostSample from models.models.project import ProjectId from models.models.sample import sample_id_transform_to_raw from models.utils.cohort_id_format import cohort_id_format, cohort_id_transform_to_raw @@ -90,6 +91,28 @@ async def m(info: Info) -> list[str]: GraphQLAnalysisStatus = strawberry.enum(AnalysisStatus) +@strawberry.experimental.pydantic.type(model=OurDNALostSample, all_fields=True) # type: ignore +class GraphQLOurDNALostSample: + """OurDNA Lost Sample GraphQL model to be used in OurDNA Dashboard""" + + pass # pylint: disable=unnecessary-pass + + +@strawberry.experimental.pydantic.type(model=OurDNADashboard) # type: ignore +class GraphQLOurDNADashboard: + """OurDNA Dashboard model""" + + collection_to_process_end_time: strawberry.scalars.JSON + collection_to_process_end_time_statistics: strawberry.scalars.JSON + collection_to_process_end_time_24h: strawberry.scalars.JSON + processing_times_by_site: strawberry.scalars.JSON + total_samples_by_collection_event_name: strawberry.scalars.JSON + samples_lost_after_collection: list[GraphQLOurDNALostSample] + samples_concentration_gt_1ug: strawberry.scalars.JSON + participants_consented_not_collected: list[int] + participants_signed_not_consented: list[int] + + # Create cohort GraphQL model @strawberry.type class GraphQLCohort: @@ -248,15 +271,14 @@ async def analysis_runner( @strawberry.field async def ourdna_dashboard( self, info: Info, root: 'Project' - ) -> strawberry.scalars.JSON: + ) -> 'GraphQLOurDNADashboard': connection = info.context['connection'] ourdna_layer = OurDnaDashboardLayer(connection) if not root.id: raise ValueError('Project must have an id') - ourdna_dashboard = await ourdna_layer.query( - SampleFilter(project=GenericFilter(eq=root.id)), project_id=root.id - ) - return ourdna_dashboard.to_dict() + ourdna_dashboard = await ourdna_layer.query(project_id=root.id) + # pylint: disable=no-member + return GraphQLOurDNADashboard.from_pydantic(ourdna_dashboard) @strawberry.field() async def pedigree( diff --git a/db/python/layers/ourdna/dashboard.py b/db/python/layers/ourdna/dashboard.py index 648fadd1f..81d6ea5f4 100644 --- a/db/python/layers/ourdna/dashboard.py +++ b/db/python/layers/ourdna/dashboard.py @@ -1,5 +1,5 @@ # pylint: disable=too-many-locals -import json +import asyncio from collections import defaultdict from datetime import datetime from math import ceil @@ -10,7 +10,9 @@ from db.python.layers.participant import ParticipantLayer from db.python.layers.sample import SampleLayer from db.python.tables.sample import SampleFilter -from models.models import OurDNADashboard, ProjectId, Sample +from db.python.utils import GenericFilter +from models.models import OurDNADashboard, OurDNALostSample, ProjectId, Sample +from models.models.participant import ParticipantInternal class OurDnaDashboardLayer(BaseLayer): @@ -101,38 +103,32 @@ def get_collection_to_process_start_time(self, sample: Sample) -> int | None: return int(time_taken.total_seconds()) - def fetch_key_from_meta(self, key: str, meta: str | dict[str, Any]) -> bool: - """ - Fetches a key from the given meta, if it exists, and checks if it is not False or None - """ - if isinstance(meta, str): - meta = json.loads(meta) - if isinstance(meta, dict): - if key in meta: - value = meta.get(key) - if value is not False and value is not None: - return True - return False - async def query( self, - filter_: SampleFilter, - project_id: ProjectId = None, + project_id: ProjectId, ) -> OurDNADashboard: """Get dashboard data""" samples: list[Sample] = [] - participants: list[tuple[int, dict, dict]] = [] + participants: list[ParticipantInternal] = [] - samples = [ - s.to_external() for s in await self.sample_layer.query(filter_=filter_) - ] - - participants = ( - await self.participant_layer.get_participants_and_samples_meta_by_project( - project=project_id - ) + s, participants = await asyncio.gather( + self.sample_layer.query( + filter_=SampleFilter(project=GenericFilter(eq=project_id)) + ), + self.participant_layer.get_participants(project=project_id), ) + # Converting to external to show stats per sample (with XPG ID) via the GraphQL API + samples = [sample.to_external() for sample in s] + participants_by_id = {p.id: p for p in participants} + + grouped_participant_samples: dict[int, list] = defaultdict(list) + + # Group instances of A by their foreign key + for sample in samples: + if sample.participant_id: + grouped_participant_samples[sample.participant_id].append(sample) + # Data to be returned collection_to_process_end_time: dict[str, int] = ( self.process_collection_to_process_end_times(samples=samples) @@ -151,31 +147,33 @@ async def query( total_samples_by_collection_event_name: dict[str, int] = ( self.process_total_samples_by_collection_event_name(samples=samples) ) - samples_lost_after_collection: dict[str, dict[str, Any]] = ( + samples_lost_after_collection: list[OurDNALostSample] = ( self.process_samples_lost_after_collection(samples=samples) ) samples_concentration_gt_1ug: dict[str, float] = ( self.process_samples_concentration_gt_1ug(samples=samples) ) participants_consented_not_collected: list[int] = ( - self.process_participants_consented_not_collected(participants) + self.process_participants_consented_not_collected( + participants_by_id, grouped_participant_samples + ) ) participants_signed_not_consented: list[int] = ( - self.process_participants_signed_not_consented(participants) + self.process_participants_signed_not_consented( + participants_by_id, grouped_participant_samples + ) ) - return OurDNADashboard.from_sample( - d={ - 'collection_to_process_end_time': collection_to_process_end_time, - 'collection_to_process_end_time_statistics': collection_to_process_end_time_statistics, - 'collection_to_process_end_time_24h': collection_to_process_end_time_24h, - 'processing_times_by_site': processing_times_by_site, - 'total_samples_by_collection_event_name': total_samples_by_collection_event_name, - 'samples_lost_after_collection': samples_lost_after_collection, - 'samples_concentration_gt_1ug': samples_concentration_gt_1ug, - 'participants_consented_not_collected': participants_consented_not_collected, - 'participants_signed_not_consented': participants_signed_not_consented, - } + return OurDNADashboard( + collection_to_process_end_time=collection_to_process_end_time, + collection_to_process_end_time_statistics=collection_to_process_end_time_statistics, + collection_to_process_end_time_24h=collection_to_process_end_time_24h, + processing_times_by_site=processing_times_by_site, + total_samples_by_collection_event_name=total_samples_by_collection_event_name, + samples_lost_after_collection=samples_lost_after_collection, + samples_concentration_gt_1ug=samples_concentration_gt_1ug, + participants_consented_not_collected=participants_consented_not_collected, + participants_signed_not_consented=participants_signed_not_consented, ) def process_collection_to_process_end_times(self, samples: list[Sample]) -> dict: @@ -268,9 +266,11 @@ def process_total_samples_by_collection_event_name( return total_samples_by_collection_event_name - def process_samples_lost_after_collection(self, samples: list[Sample]) -> dict: + def process_samples_lost_after_collection( + self, samples: list[Sample] + ) -> list[OurDNALostSample]: """Get total number of many samples have been lost, EG: participants have been consented, blood collected, not processed (etc), Alert here (highlight after 72 hours)""" - samples_lost_after_collection: dict[str, dict[str, Any]] = {} + samples_lost_after_collection: list[OurDNALostSample] = [] for sample in samples: time_to_process_start = self.get_collection_to_process_start_time(sample) @@ -279,45 +279,49 @@ def process_samples_lost_after_collection(self, samples: list[Sample]) -> dict: time_to_process_start is not None and time_to_process_start > 72 * 60 * 60 ): - samples_lost_after_collection[sample.id] = { - 'time_to_process_start': time_to_process_start, - 'collection_time': self.get_meta_property( - sample=sample, property_name='collection-time' - ), - 'process_start_time': self.get_meta_property( - sample=sample, property_name='process-start-time' - ), - 'process_end_time': self.get_meta_property( - sample=sample, property_name='process-end-time' - ), - 'received_time': self.get_meta_property( - sample=sample, property_name='received-time' - ), - 'received_by': self.get_meta_property( - sample=sample, property_name='received-by' - ), - 'collection_lab': self.get_meta_property( - sample=sample, property_name='collection-lab' - ), - 'courier': self.get_meta_property( - sample=sample, property_name='courier' - ), - 'courier_tracking_number': self.get_meta_property( - sample=sample, property_name='courier-tracking-number' - ), - 'courier_scheduled_pickup_time': self.get_meta_property( - sample=sample, property_name='courier-scheduled-pickup-time' - ), - 'courier_actual_pickup_time': self.get_meta_property( - sample=sample, property_name='courier-actual-pickup-time' - ), - 'courier_scheduled_dropoff_time': self.get_meta_property( - sample=sample, property_name='courier-scheduled-dropoff-time' - ), - 'courier_actual_dropoff_time': self.get_meta_property( - sample=sample, property_name='courier-actual-dropoff-time' - ), - } + samples_lost_after_collection.append( + OurDNALostSample( + sample_id=sample.id, + time_to_process_start=time_to_process_start, + collection_time=self.get_meta_property( + sample=sample, property_name='collection-time' + ), + process_start_time=self.get_meta_property( + sample=sample, property_name='process-start-time' + ), + process_end_time=self.get_meta_property( + sample=sample, property_name='process-end-time' + ), + received_time=self.get_meta_property( + sample=sample, property_name='received-time' + ), + received_by=self.get_meta_property( + sample=sample, property_name='received-by' + ), + collection_lab=self.get_meta_property( + sample=sample, property_name='collection-lab' + ), + courier=self.get_meta_property( + sample=sample, property_name='courier' + ), + courier_tracking_number=self.get_meta_property( + sample=sample, property_name='courier-tracking-number' + ), + courier_scheduled_pickup_time=self.get_meta_property( + sample=sample, property_name='courier-scheduled-pickup-time' + ), + courier_actual_pickup_time=self.get_meta_property( + sample=sample, property_name='courier-actual-pickup-time' + ), + courier_scheduled_dropoff_time=self.get_meta_property( + sample=sample, + property_name='courier-scheduled-dropoff-time', + ), + courier_actual_dropoff_time=self.get_meta_property( + sample=sample, property_name='courier-actual-dropoff-time' + ), + ) + ) return samples_lost_after_collection @@ -327,30 +331,41 @@ def process_samples_concentration_gt_1ug(self, samples: list[Sample]) -> dict: for sample in samples: if ( - sample.meta.get('concentration') is not None - and float(sample.meta.get('concentration')) > 1 + sample.meta.get('concentration') + and float(sample.meta['concentration']) > 1 ): samples_concentration_gt_1ug[sample.id] = float( - sample.meta.get('concentration') + sample.meta['concentration'] ) return samples_concentration_gt_1ug def process_participants_consented_not_collected( - self, participants: list[tuple[int, dict, dict]] + self, + participants: dict[int, ParticipantInternal], + grouped_participants_samples: dict[int, list[Sample]], ) -> list[int]: """Get the participants who have been consented but have not had a sample collected""" - return [ - p[0] - for p in participants - if self.fetch_key_from_meta('consent', p[1]) - and not self.fetch_key_from_meta('collection-time', p[2]) - ] + filtered_participants: list[int] = [] + for participant_id, samples in grouped_participants_samples.items(): + participant = participants[participant_id] + if participant.meta.get('consent') and any( + sample.meta.get('collection-time') is None for sample in samples + ): + filtered_participants.append(participant.id) + + return filtered_participants def process_participants_signed_not_consented( - self, participants: list[tuple[int, dict, dict]] + self, + participants: dict[int, ParticipantInternal], + grouped_participants_samples: dict[int, list[Sample]], ) -> list[int]: """Get the participants who have signed but have not been consented""" - return [ - p[0] for p in participants if not self.fetch_key_from_meta('consent', p[1]) - ] + filtered_participants: list[int] = [] + for participant_id, _ in grouped_participants_samples.items(): + participant = participants[participant_id] + if not participant.meta.get('consent'): + filtered_participants.append(participant.id) + + return filtered_participants diff --git a/db/python/layers/participant.py b/db/python/layers/participant.py index 77d014a80..6b1bffd71 100644 --- a/db/python/layers/participant.py +++ b/db/python/layers/participant.py @@ -981,9 +981,3 @@ async def update_participant_family( maternal_id=fp_row.maternal_id, affected=fp_row.affected, ) - - async def get_participants_and_samples_meta_by_project( - self, project: ProjectId - ) -> list[tuple[int, dict, dict]]: - """Get participants who have consented but not collected samples""" - return await self.pttable.get_participants_and_samples_meta_by_project(project) diff --git a/db/python/tables/participant.py b/db/python/tables/participant.py index 521a1120e..60d6c7111 100644 --- a/db/python/tables/participant.py +++ b/db/python/tables/participant.py @@ -343,20 +343,3 @@ async def search( }, ) return [(r['project'], r['id'], r['external_id']) for r in rows] - - async def get_participants_and_samples_meta_by_project( - self, project: ProjectId - ) -> list[tuple[int, dict, dict]]: - """ - Get participants who have consented but not collected - """ - _query = """ - SELECT p.id as participant_id, p.meta as participant_meta, s.meta as sample_meta - FROM participant p - LEFT JOIN sample s ON p.id = s.participant_id - WHERE p.project = :project - """ - rows = await self.connection.fetch_all(_query, {'project': project}) - return [ - (r['participant_id'], r['participant_meta'], r['sample_meta']) for r in rows - ] diff --git a/models/models/__init__.py b/models/models/__init__.py index 9dbba271f..ac0a9e3f2 100644 --- a/models/models/__init__.py +++ b/models/models/__init__.py @@ -28,7 +28,7 @@ FamilySimpleInternal, PedRowInternal, ) -from models.models.ourdna import OurDNADashboard +from models.models.ourdna import OurDNADashboard, OurDNALostSample from models.models.participant import ( NestedParticipant, NestedParticipantInternal, diff --git a/models/models/ourdna.py b/models/models/ourdna.py index 37bfde355..6910e6e68 100644 --- a/models/models/ourdna.py +++ b/models/models/ourdna.py @@ -1,9 +1,27 @@ from collections import defaultdict -from typing import Any from pydantic import BaseModel +class OurDNALostSample(BaseModel): + """Model for OurDNA Lost Sample""" + + sample_id: str + time_to_process_start: int + collection_time: str + process_start_time: str + process_end_time: str + received_time: str + received_by: str + collection_lab: str + courier: str + courier_tracking_number: str + courier_scheduled_pickup_time: str + courier_actual_pickup_time: str + courier_scheduled_dropoff_time: str + courier_actual_dropoff_time: str + + class OurDNADashboard(BaseModel): """Model for OurDNA Dashboard""" @@ -14,60 +32,7 @@ class OurDNADashboard(BaseModel): lambda: defaultdict(int) ) total_samples_by_collection_event_name: dict[str, int] = defaultdict(int) - samples_lost_after_collection: dict[str, dict[str, Any]] = {} + samples_lost_after_collection: list[OurDNALostSample] = [] samples_concentration_gt_1ug: dict[str, float] = {} participants_consented_not_collected: list[int] = [] participants_signed_not_consented: list[int] = [] - - @staticmethod - def from_sample(d: dict) -> 'OurDNADashboard': - """ - Convert from a sample object - """ - collection_to_process_end_time = d.pop('collection_to_process_end_time', {}) - collection_to_process_end_time_statistics = d.pop( - 'collection_to_process_end_time_statistics', {} - ) - collection_to_process_end_time_24h = d.pop( - 'collection_to_process_end_time_24h', {} - ) - processing_times_by_site = d.pop('processing_times_by_site', {}) - total_samples_by_collection_event_name = d.pop( - 'total_samples_by_collection_event_name', {} - ) - samples_lost_after_collection = d.pop('samples_lost_after_collection', {}) - samples_concentration_gt_1ug = d.pop('samples_concentration_gt_1ug', {}) - participants_consented_not_collected = d.pop( - 'participants_consented_not_collected', [] - ) - participants_signed_not_consented = d.pop( - 'participants_signed_not_consented', [] - ) - - return OurDNADashboard( - collection_to_process_end_time=collection_to_process_end_time, - collection_to_process_end_time_statistics=collection_to_process_end_time_statistics, - collection_to_process_end_time_24h=collection_to_process_end_time_24h, - processing_times_by_site=processing_times_by_site, - total_samples_by_collection_event_name=total_samples_by_collection_event_name, - samples_lost_after_collection=samples_lost_after_collection, - samples_concentration_gt_1ug=samples_concentration_gt_1ug, - participants_consented_not_collected=participants_consented_not_collected, - participants_signed_not_consented=participants_signed_not_consented, - ) - - def to_dict(self) -> dict: - """ - Convert to a dictionary - """ - return { - 'collection_to_process_end_time': self.collection_to_process_end_time, - 'collection_to_process_end_time_statistics': self.collection_to_process_end_time_statistics, - 'collection_to_process_end_time_24h': self.collection_to_process_end_time_24h, - 'processing_times_by_site': self.processing_times_by_site, - 'total_samples_by_collection_event_name': self.total_samples_by_collection_event_name, - 'samples_lost_after_collection': self.samples_lost_after_collection, - 'samples_concentration_gt_1ug': self.samples_concentration_gt_1ug, - 'participants_consented_not_collected': self.participants_consented_not_collected, - 'participants_signed_not_consented': self.participants_signed_not_consented, - } diff --git a/test/test_ourdna_dashboard.py b/test/test_ourdna_dashboard.py index 1231dc75c..0f529d505 100644 --- a/test/test_ourdna_dashboard.py +++ b/test/test_ourdna_dashboard.py @@ -6,9 +6,8 @@ from db.python.layers.ourdna.dashboard import OurDnaDashboardLayer from db.python.layers.participant import ParticipantLayer from db.python.layers.sample import SampleLayer -from db.python.tables.sample import SampleFilter -from db.python.utils import GenericFilter from models.models import ( + OurDNADashboard, ParticipantUpsert, ParticipantUpsertInternal, SampleUpsert, @@ -143,19 +142,17 @@ async def setUp(self) -> None: @run_as_sync async def test_get_dashboard(self): """Test get_dashboard""" - sample_filter = SampleFilter(project=GenericFilter(eq=self.project_id)) - dashboard = (await self.odd.query(sample_filter)).to_dict() + dashboard = await self.odd.query(project_id=self.project_id) # Check that the dashboard is not empty and is a dict assert dashboard - assert isinstance(dashboard, dict) + assert isinstance(dashboard, OurDNADashboard) @run_as_sync async def test_collection_to_process_end_time(self): """I want to know how long it took between blood collection and sample processing""" - sample_filter = SampleFilter(project=GenericFilter(eq=self.project_id)) - dashboard = (await self.odd.query(sample_filter)).to_dict() - collection_to_process_end_time = dashboard['collection_to_process_end_time'] + dashboard = await self.odd.query(project_id=self.project_id) + collection_to_process_end_time = dashboard.collection_to_process_end_time # Check that collection_to_process_end_time is not empty and is a dict assert collection_to_process_end_time @@ -176,6 +173,7 @@ async def test_collection_to_process_end_time(self): samples_filtered.append(sample) # Check that the time difference matches + assert isinstance(sample.id, str) assert ( time_difference.total_seconds() == collection_to_process_end_time[sample.id] @@ -196,10 +194,9 @@ async def test_collection_to_process_end_time(self): @run_as_sync async def test_collection_to_process_end_time_24h(self): """I want to know which samples took more than 24 hours between blood collection and sample processing completion""" - sample_filter = SampleFilter(project=GenericFilter(eq=self.project_id)) - dashboard = (await self.odd.query(sample_filter)).to_dict() - collection_to_process_end_time_24h = dashboard.get( - 'collection_to_process_end_time_24h' + dashboard = await self.odd.query(project_id=self.project_id) + collection_to_process_end_time_24h = ( + dashboard.collection_to_process_end_time_24h ) # Check that collection_to_process_end_time is not empty and is a dict @@ -221,6 +218,7 @@ async def test_collection_to_process_end_time_24h(self): samples_filtered.append(sample) # Check that the time difference matches + assert isinstance(sample.id, str) assert ( time_difference.total_seconds() == collection_to_process_end_time_24h[sample.id] @@ -235,15 +233,14 @@ async def test_collection_to_process_end_time_24h(self): @run_as_sync async def test_processing_times_per_site(self): """I want to know what the sample processing times were for samples at each designated site""" - sample_filter = SampleFilter(project=GenericFilter(eq=self.project_id)) - dashboard = (await self.odd.query(sample_filter)).to_dict() - processing_times_by_site = dashboard.get('processing_times_by_site') + dashboard = await self.odd.query(project_id=self.project_id) + processing_times_by_site = dashboard.processing_times_by_site # Check that processing_times_per_site is not empty and is a dict assert processing_times_by_site assert isinstance(processing_times_by_site, dict) - sample_tally: dict[str, dict[str, int]] = OrderedDict() + sample_tally: dict[str, dict[int, int]] = OrderedDict() for sample in self.sample_external_objects: assert isinstance(sample.meta, dict) processing_site = sample.meta.get('processing-site', 'Unknown') @@ -268,10 +265,9 @@ async def test_processing_times_per_site(self): @run_as_sync async def test_total_samples_by_collection_event_name(self): """I want to know how many samples were collected from walk-ins vs during events or scheduled activities""" - sample_filter = SampleFilter(project=GenericFilter(eq=self.project_id)) - dashboard = (await self.odd.query(sample_filter)).to_dict() - total_samples_by_collection_event_name = dashboard.get( - 'total_samples_by_collection_event_name' + dashboard = await self.odd.query(project_id=self.project_id) + total_samples_by_collection_event_name = ( + dashboard.total_samples_by_collection_event_name ) # Check that total_samples_by_collection_event_name is not empty and is a dict @@ -293,9 +289,8 @@ async def test_total_samples_by_collection_event_name(self): @run_as_sync async def test_samples_lost_after_collection(self): """I need to know how many samples have been lost, EG: participants have been consented, blood collected, not processed""" - sample_filter = SampleFilter(project=GenericFilter(eq=self.project_id)) - dashboard = (await self.odd.query(sample_filter)).to_dict() - samples_lost_after_collection = dashboard.get('samples_lost_after_collection') + dashboard = await self.odd.query(project_id=self.project_id) + samples_lost_after_collection = dashboard.samples_lost_after_collection # Check that samples_lost_after_collection is not empty and is a dict assert samples_lost_after_collection @@ -317,6 +312,7 @@ async def test_samples_lost_after_collection(self): samples_filtered.append(sample) # Check that the time difference matches + assert isinstance(sample.id, str) assert ( time_difference.total_seconds() == samples_lost_after_collection[sample.id]['time_to_process_start'] @@ -333,9 +329,8 @@ async def test_samples_lost_after_collection(self): @run_as_sync async def test_samples_more_than_1ug_dna(self): """I want to generate a list of samples containing more than 1 ug of DNA to prioritise them for long-read sequencing applications""" - sample_filter = SampleFilter(project=GenericFilter(eq=self.project_id)) - dashboard = (await self.odd.query(sample_filter)).to_dict() - samples_more_than_1ug_dna = dashboard.get('samples_concentration_gt_1ug') + dashboard = await self.odd.query(project_id=self.project_id) + samples_more_than_1ug_dna = dashboard.samples_concentration_gt_1ug # Check that samples_concentratiom_gt_1ug is not empty and is a dict assert samples_more_than_1ug_dna @@ -345,10 +340,7 @@ async def test_samples_more_than_1ug_dna(self): samples_filtered: list[SampleUpsert] = [] for sample in self.sample_external_objects: assert isinstance(sample.meta, dict) - if ( - sample.meta.get('concentration') - and sample.meta.get('concentration') > 1 - ): + if sample.meta['concentration'] and sample.meta['concentration'] > 1: samples_filtered.append(sample) # Check that the sample id is in the dict @@ -359,16 +351,13 @@ async def test_samples_more_than_1ug_dna(self): @run_as_sync async def test_participants_consented_not_collected(self): """I want to know how many people who have consented and NOT given blood""" - sample_filter = SampleFilter(project=GenericFilter(eq=self.project_id)) - dashboard = ( - await self.odd.query(sample_filter, project_id=self.project_id) - ).to_dict() + dashboard = await self.odd.query(project_id=self.project_id) # print(dashboard) - participants_consented_not_collected = dashboard.get( - 'participants_consented_not_collected' + participants_consented_not_collected = ( + dashboard.participants_consented_not_collected ) - # Check that participants_signed_not_consented is not empty and is a dict + # Check that participants_consented_not_collected is not empty and is a dict assert participants_consented_not_collected assert isinstance(participants_consented_not_collected, list) @@ -382,8 +371,9 @@ async def test_participants_consented_not_collected(self): if sample.participant_id == participant.id and isinstance(sample.meta, dict) ] - if participant.meta.get('consent') and not any( - isinstance(sample.meta, dict) and sample.meta.get('collection-time') + if participant.meta.get('consent') and any( + isinstance(sample.meta, dict) + and sample.meta.get('collection-time') is None for sample in samples_for_participant ): participants_filtered.append(participant) @@ -396,14 +386,9 @@ async def test_participants_consented_not_collected(self): @run_as_sync async def test_participants_signed_not_consented(self): """I want to know how many people have signed up but not consented""" - sample_filter = SampleFilter(project=GenericFilter(eq=self.project_id)) - dashboard = ( - await self.odd.query(sample_filter, project_id=self.project_id) - ).to_dict() + dashboard = await self.odd.query(project_id=self.project_id) # print(dashboard) - participants_signed_not_consented = dashboard.get( - 'participants_signed_not_consented' - ) + participants_signed_not_consented = dashboard.participants_signed_not_consented # Check that participants_signed_not_consented is not empty and is a dict assert participants_signed_not_consented