Skip to content

Commit

Permalink
Merge pull request #470 from wasade/scrub-bug
Browse files Browse the repository at this point in the history
Scrub bug
  • Loading branch information
cassidysymons authored Oct 28, 2022
2 parents 579cee5 + 373469b commit f0bdae3
Show file tree
Hide file tree
Showing 4 changed files with 384 additions and 7 deletions.
23 changes: 16 additions & 7 deletions microsetta_private_api/admin/admin_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,7 @@ def delete_account(account_id, token_info):
src_repo = SourceRepo(t)
samp_repo = SampleRepo(t)
sar_repo = SurveyAnswersRepo(t)
template_repo = SurveyTemplateRepo(t)

acct = acct_repo.get_account(account_id)
if acct is None:
Expand All @@ -806,35 +807,43 @@ def delete_account(account_id, token_info):
return None, 204

sample_count = 0
account_has_external = False
sources = src_repo.get_sources_in_account(account_id)

for source in sources:
samples = samp_repo.get_samples_by_source(account_id, source.id)

has_samples = len(samples) > 0
sample_count += len(samples)
has_external = template_repo.has_external_surveys(account_id,
source.id)

if has_external:
account_has_external = True

for sample in samples:
# we scrub rather than disassociate in the event that the
# sample is in our freezers but not with an up-to-date scan
samp_repo.scrub(account_id, source.id, sample.id)

surveys = sar_repo.list_answered_surveys(account_id, source.id)
if has_samples:
# if we have samples, we need to scrub survey / source
# free text
if has_samples or has_external:
# if we have samples or external surveys, we need to scrub
# survey / source free text
for survey_id in surveys:
sar_repo.scrub(account_id, source.id, survey_id)
src_repo.scrub(account_id, source.id)
else:
# if we do not have associated samples, then the source
# is safe to delete

if not has_samples and not has_external:
# if we do not have associated samples, or external surveys,
# then the source is safe to delete
for survey_id in surveys:
sar_repo.delete_answered_survey(account_id, survey_id)
src_repo.delete_source(account_id, source.id)

# an account is safe to delete if there are no associated samples
if sample_count > 0:
# and does not have external surveys
if sample_count > 0 or account_has_external:
acct_repo.scrub(account_id)
else:
acct_repo.delete_account(account_id)
Expand Down
50 changes: 50 additions & 0 deletions microsetta_private_api/api/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from microsetta_private_api.repo.account_repo import AccountRepo
from microsetta_private_api.repo.source_repo import SourceRepo
from microsetta_private_api.repo.survey_answers_repo import SurveyAnswersRepo
from microsetta_private_api.repo.survey_template_repo import SurveyTemplateRepo
from microsetta_private_api.repo.sample_repo import SampleRepo
from microsetta_private_api.repo.vioscreen_repo import (
VioscreenSessionRepo, VioscreenPercentEnergyRepo,
Expand Down Expand Up @@ -311,6 +312,7 @@ def delete_dummy_accts():
survey_answers_repo = SurveyAnswersRepo(t)
sample_repo = SampleRepo(t)
barcode_repo = BarcodeRepo(t)
template_repo = SurveyTemplateRepo(t)

# Delete fake kit and barcode preps
barcode_repo.delete_preparation(BC1, 1234)
Expand All @@ -330,6 +332,17 @@ def delete_dummy_accts():
sample_ids = [x.id for x in source_samples]
all_sample_ids.extend(sample_ids)

# Dissociate any secondary surveys.
# IMPORTANT: the order of operations here matters. It is
# necessary to delete external surveys PRIOR to deleting survey
# answers as the survey answers deletion will set source_id to
# NULL for entries in the registries
template_repo.delete_myfoodrepo(curr_acct_id, curr_source.id)
template_repo.delete_vioscreen(curr_acct_id, curr_source.id)
template_repo.delete_polyphenol_ffq(curr_acct_id,
curr_source.id)
template_repo.delete_spain_ffq(curr_acct_id, curr_source.id)

# Dissociate all samples linked to this source from all
# answered surveys linked to this source, then delete all
# answered surveys
Expand Down Expand Up @@ -1051,6 +1064,43 @@ def test_account_scrub_non_admin(self):
# check response code
self.assertEqual(401, response.status_code)

def test_account_scrub_no_samples_has_secondary_survey_bug(self):
# setup an account with a source and sample
dummy_acct_id, dummy_source_id = create_dummy_source(
"Bo", Source.SOURCE_TYPE_HUMAN, DUMMY_HUMAN_SOURCE,
create_dummy_1=True)

_ = create_dummy_acct(create_dummy_1=False,
iss=ACCT_MOCK_ISS_3,
sub=ACCT_MOCK_SUB_3,
dummy_is_admin=True)

# "take" the polyphenol FFQ
response = self.client.get(
'/api/accounts/%s/sources/%s/survey_templates/%s?language_tag=en_us' % # noqa
(dummy_acct_id, dummy_source_id,
SurveyTemplateRepo.POLYPHENOL_FFQ_ID),
headers=self.dummy_auth)

# now let's scrub it
response = self.client.delete(
'/api/accounts/%s' %
(dummy_acct_id,),
headers=make_headers(FAKE_TOKEN_ADMIN))

# check response code
self.assertEqual(204, response.status_code)

# verify deleting is idempotent. If the account was scrubbed, then
# we get a 204. If the account was deleted, we get a 404
response = self.client.delete(
'/api/accounts/%s' %
(dummy_acct_id,),
headers=make_headers(FAKE_TOKEN_ADMIN))

# check response code
self.assertEqual(204, response.status_code)

# region account view/get tests
def test_account_view_success(self):
"""Successfully view existing account"""
Expand Down
211 changes: 211 additions & 0 deletions microsetta_private_api/repo/survey_template_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,38 @@ def set_myfoodrepo_id(self, account_id, source_id, mfr_id):
WHERE account_id=%s AND source_id=%s""",
(mfr_id, account_id, source_id))

def delete_myfoodrepo(self, account_id, source_id):
"""Intended for admin use, remove MyFoodRepo entries
This method is idempotent
This method deletes ALL MyFoodRepo surveys associated with an account
and source
This is a hard delete, we REMOVE rows rather than setting a flag
Parameters
----------
account_id : str, UUID
The account UUID
source_id : str, UUID
The source UUID
"""
with self._transaction.cursor() as cur:
existing, created = self.get_myfoodrepo_id_if_exists(account_id,
source_id)
if existing is not None:
cur.execute("""DELETE FROM ag.ag_login_surveys
WHERE ag_login_id=%s
AND source_id=%s
AND survey_id=%s""",
(account_id, source_id, existing))

cur.execute("""DELETE FROM ag.myfoodrepo_registry
WHERE account_id=%s
AND source_id=%s""",
(account_id, source_id))

def get_myfoodrepo_id_if_exists(self, account_id, source_id):
"""Return a MyFoodRepo ID if one exists
Expand Down Expand Up @@ -467,6 +499,37 @@ def get_polyphenol_ffq_id_if_exists(self, account_id, source_id):
else:
return res

def delete_polyphenol_ffq(self, account_id, source_id):
"""Intended for admin use, remove Polyphenol FFQ entries
This method is idempotent.
This method deletes ALL Polyphenol FFQ surveys associated with an
account and source
This is a hard delete, we REMOVE rows rather than setting a flag
Parameters
----------
account_id : str, UUID
The account UUID
source_id : str, UUID
The source UUID
"""
with self._transaction.cursor() as cur:
existing, _ = self.get_polyphenol_ffq_id_if_exists(account_id,
source_id)
if existing is not None:
cur.execute("""DELETE FROM ag.ag_login_surveys
WHERE ag_login_id=%s
AND source_id=%s
AND survey_id=%s""",
(account_id, source_id, existing))
cur.execute("""DELETE FROM ag.polyphenol_ffq_registry
WHERE account_id=%s
AND source_id=%s""",
(account_id, source_id))

def create_spain_ffq_entry(self, account_id, source_id):
"""Return a newly created Spain FFQ ID
Expand Down Expand Up @@ -530,6 +593,38 @@ def get_spain_ffq_id_if_exists(self, account_id, source_id):
else:
return res[0]

def delete_spain_ffq(self, account_id, source_id):
"""Intended for admin use, remove Spain FFQ entries
This method is idempotent
This method deletes ALL Spain FFQ surveys associated with an account
and source
This is a hard delete, we REMOVE rows rather than setting a flag
Parameters
----------
account_id : str, UUID
The account UUID
source_id : str, UUID
The source UUID
"""
with self._transaction.cursor() as cur:
existing = self.get_spain_ffq_id_if_exists(account_id,
source_id)
if existing is not None:
cur.execute("""DELETE FROM ag.ag_login_surveys
WHERE ag_login_id=%s
AND source_id=%s
AND survey_id=%s""",
(account_id, source_id, existing))

cur.execute("""DELETE FROM ag.spain_ffq_registry
WHERE account_id=%s
AND source_id=%s""",
(account_id, source_id))

def get_vioscreen_sample_to_user(self):
"""Obtain a mapping of sample barcode to vioscreen user"""
with self._transaction.cursor() as cur:
Expand Down Expand Up @@ -605,6 +700,88 @@ def get_vioscreen_id_if_exists(self, account_id, source_id,
else:
return rows[0][0]

def get_vioscreen_all_ids_if_exists(self, account_id, source_id):
"""Obtain all vioscreen IDs for a source
This method captures all IDs including IDs with the "deleted"
flag set.
Parameters
----------
account_id : str, UUID
The account UUID
source_id : str, UUID
The source UUID
Returns
tuple or None
The tuple of IDs or None of there are no associated IDs
"""
with self._transaction.cursor() as cur:
cur.execute("""SELECT vio_id
FROM vioscreen_registry
WHERE account_id = %s
AND source_id = %s""",
(account_id, source_id))
ids = tuple([r[0] for r in cur.fetchall()])
if len(ids) > 0:
return ids
else:
return None

def delete_vioscreen(self, account_id, source_id):
"""Intended for admin use, remove Vioscreen entries from the system
This method is idempotent
This method deletes ALL Vioscreen FFQ surveys associated with an
account and source
This is a hard delete, we REMOVE rows rather than setting a flag
Parameters
----------
account_id : str, UUID
The account UUID
source_id : str, UUID
The source UUID
"""
with self._transaction.cursor() as cur:
# get all vioscreen user names associated with the source
cur.execute("""SELECT vio_id
FROM vioscreen_registry
WHERE account_id=%s
AND source_id=%s""",
(account_id, source_id))
vio_ids = tuple([r[0] for r in cur.fetchall()])

if len(vio_ids) == 0:
return None

# get all sessions associated with the vio_ids
cur.execute("""SELECT sessionid
FROM ag.vioscreen_sessions
WHERE username IN %s""",
(vio_ids, ))
sessions = tuple([r[0] for r in cur.fetchall()])

if len(sessions) > 0:
for tbl in ('dietaryscore', 'eatingpatterns', 'foodcomponents',
'foodconsumption', 'foodconsumptioncomponents',
'percentenergy', 'supplements', 'mpeds'):
tbl = f'ag.vioscreen_{tbl}'
cur.execute("DELETE FROM " + tbl + " " +
"WHERE sessionid IN %s",
(sessions, ))
cur.execute("""DELETE FROM ag.vioscreen_sessions
WHERE sessionid IN %s""",
(sessions, ))

cur.execute("""DELETE FROM ag.vioscreen_registry
WHERE account_id = %s
AND source_id = %s""",
(account_id, source_id))

def fetch_user_basic_physiology(self, account_id, source_id):
"""Given an account and source ID, obtain basic physiology properties
Expand Down Expand Up @@ -718,3 +895,37 @@ def fetch_user_basic_physiology(self, account_id, source_id):
weight = None

return (birth_year, gender, height, weight)

def has_external_surveys(self, account_id, source_id):
"""Test whether a source has any external surveys associated
Parameters
----------
account_id : str, UUID
The account UUID
source_id : str, UUID
The source UUID
Returns
-------
boolean
True indicates the user has an external survey associated, false
otherwise
"""
getters = (self.get_myfoodrepo_id_if_exists,
self.get_polyphenol_ffq_id_if_exists,
self.get_spain_ffq_id_if_exists,
self.get_vioscreen_all_ids_if_exists)

for get in getters:
res = get(account_id, source_id)

# the if_exists methods are inconsistent in return type, yay
if isinstance(res, tuple):
if res[0] is not None:
return True
else:
if res is not None:
return True

return False
Loading

0 comments on commit f0bdae3

Please sign in to comment.