Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "TN-3324 robust handling of unsortable patient list attributes" #4417

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 42 additions & 46 deletions portal/models/patient_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,53 +53,49 @@ def patient_list_update_patient(patient_id, research_study_id=None):
if not user.has_role(ROLE.PATIENT.value):
return

from ..timeout_lock import TimeoutLock
# async possibility, only allow one thread at a time
key = f"patient_list_update_patient:{patient_id}"
with TimeoutLock(key, expires=60, timeout=60):
patient = PatientList.query.get(patient_id)
new_record = False
if not patient:
new_record = True
patient = PatientList(userid=patient_id)
db.session.add(patient)
patient = PatientList.query.get(patient_id)
new_record = False
if not patient:
new_record = True
patient = PatientList(userid=patient_id)
db.session.add(patient)

if research_study_id is None or new_record:
patient.study_id = user.external_study_id
patient.firstname = user.first_name
patient.lastname = user.last_name
patient.email = user.email
patient.birthdate = user.birthdate
patient.deleted = user.deleted_id is not None
patient.test_role = True if user.has_role(ROLE.TEST.value) else False
patient.org_id = user.organizations[0].id if user.organizations else None
patient.org_name = user.organizations[0].name if user.organizations else None
if research_study_id is None or new_record:
patient.study_id = user.external_study_id
patient.firstname = user.first_name
patient.lastname = user.last_name
patient.email = user.email
patient.birthdate = user.birthdate
patient.deleted = user.deleted_id is not None
patient.test_role = True if user.has_role(ROLE.TEST.value) else False
patient.org_id = user.organizations[0].id if user.organizations else None
patient.org_name = user.organizations[0].name if user.organizations else None

# necessary to avoid recursive loop via some update paths
now = datetime.utcnow()
if patient.last_updated and patient.last_updated + timedelta(seconds=10) > now:
db.session.commit()
return
# necessary to avoid recursive loop via some update paths
now = datetime.utcnow()
if patient.last_updated and patient.last_updated + timedelta(seconds=10) > now:
db.session.commit()
return

patient.last_updated = now
if research_study_id == BASE_RS_ID or research_study_id is None:
rs_id = BASE_RS_ID
qb_status = qb_status_visit_name(
patient.userid, research_study_id=rs_id, as_of_date=now)
patient.questionnaire_status = str(qb_status['status'])
patient.visit = qb_status['visit_name']
patient.consentdate, _ = consent_withdrawal_dates(user=user, research_study_id=rs_id)
patient.last_updated = now
if research_study_id == BASE_RS_ID or research_study_id is None:
rs_id = BASE_RS_ID
qb_status = qb_status_visit_name(
patient.userid, research_study_id=rs_id, as_of_date=now)
patient.questionnaire_status = str(qb_status['status'])
patient.visit = qb_status['visit_name']
patient.consentdate, _ = consent_withdrawal_dates(user=user, research_study_id=rs_id)

if (research_study_id == EMPRO_RS_ID or research_study_id is None) and user.clinicians:
rs_id = EMPRO_RS_ID
patient.clinician = '; '.join(
(clinician_name_map().get(c.id, "not in map") for c in user.clinicians)) or ""
qb_status = qb_status_visit_name(
patient.userid, research_study_id=rs_id, as_of_date=now)
patient.empro_status = str(qb_status['status'])
patient.empro_visit = qb_status['visit_name']
patient.action_state = qb_status['action_state'].title() \
if qb_status['action_state'] else ""
patient.empro_consentdate, _ = consent_withdrawal_dates(
user=user, research_study_id=rs_id)
db.session.commit()
if (research_study_id == EMPRO_RS_ID or research_study_id is None) and user.clinicians:
rs_id = EMPRO_RS_ID
patient.clinician = '; '.join(
(clinician_name_map().get(c.id, "not in map") for c in user.clinicians)) or ""
qb_status = qb_status_visit_name(
patient.userid, research_study_id=rs_id, as_of_date=now)
patient.empro_status = str(qb_status['status'])
patient.empro_visit = qb_status['visit_name']
patient.action_state = qb_status['action_state'].title() \
if qb_status['action_state'] else ""
patient.empro_consentdate, _ = consent_withdrawal_dates(
user=user, research_study_id=rs_id)
db.session.commit()
4 changes: 2 additions & 2 deletions portal/models/qb_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ def _sync_timeline(self):

completed = QBT.query.filter(QBT.user_id == self.user.id).filter(
QBT.research_study_id == self.research_study_id).filter(
QBT.status == OverallStatus.completed).with_entities(QBT.id).first()
self.at_least_one_completed = completed is not None
QBT.status == OverallStatus.completed).count()
self.at_least_one_completed = completed > 0

# Obtain withdrawal date if applicable
withdrawn = QBT.query.filter(QBT.user_id == self.user.id).filter(
Expand Down
3 changes: 1 addition & 2 deletions portal/static/js/src/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -1323,8 +1323,7 @@ let requestTimerId = 0;
) {
var tnthAjax = this.getDependency("tnthAjax");
tableName = tableName || this.tableIdentifier;
if (!tableName || !document.querySelector("#adminTable")) {
if (callback) callback();
if (!tableName) {
return false;
}
userId = userId || this.userId;
Expand Down
10 changes: 0 additions & 10 deletions portal/static/js/src/modules/TnthAjax.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,16 +93,6 @@ export default { /*global $ */
$.ajax("/api/me").done(
function() {
console.log("user authorized");
if ((typeof CsrfTokenChecker !== "undefined") &&
!CsrfTokenChecker.checkTokenValidity()) {
//if CSRF Token not valid, return error
if (callback) {
callback({"error": DEFAULT_SERVER_DATA_ERROR});
fieldHelper.showError(targetField);
}
return;
}

ajaxCall();
}
).fail(function() {
Expand Down
2 changes: 0 additions & 2 deletions portal/templates/admin/admin_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,6 @@
// custom ajax request here
function patientDataAjaxRequest(params) {
loadIntervalId = setInterval(() => {
//document DOM not ready, don't make ajax call yet
if (!document.querySelector("#adminTable")) return;
if (typeof window.AdminObj === "undefined") return;
window.AdminObj.getRemotePatientListData(params);
clearInterval(loadIntervalId);
Expand Down
4 changes: 0 additions & 4 deletions portal/views/patients.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,6 @@ def filter_query(query, filter_field, filter_value):
# ignore requests to filter by unknown column
return query

if filter_field in ('birthdate', 'consentdate', 'empro_consentdate'):
# these are not filterable (partial strings on date complexity) - ignore such a request
return query

if filter_field == 'userid':
query = query.filter(PatientList.userid == int(filter_value))
return query
Expand Down
2 changes: 2 additions & 0 deletions requirements.dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pyparsing==2.4.7 # via packaging
pytest >= 6.1.0
pytest-flask==0.15.1
pytest-timeout==1.4.2
selenium==3.141.0
snowballstemmer==2.0.0 # via sphinx
sphinx-rtd-theme==0.5.1
sphinx==3.3.1
Expand All @@ -35,4 +36,5 @@ virtualenv==20.4.2 # via tox
waitress==1.4.3 # via webtest
webob==1.8.5 # via webtest
webtest==2.0.33 # via flask-webtest
xvfbwrapper==0.2.9
--editable .
141 changes: 141 additions & 0 deletions tests/integration_tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"""Unit test module for Selenium testing"""

import os
import sys
import unittest

from flask_testing import LiveServerTestCase
import pytest
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
import xvfbwrapper

from tests import TestCase


@unittest.skipUnless(
(
"SAUCE_USERNAME" in os.environ or
xvfbwrapper.Xvfb().xvfb_exists()
),
"Xvfb not installed"
)
class IntegrationTestCase(TestCase, LiveServerTestCase):
"""Test class for UI integration/workflow testing"""

def setUp(self):
"""Reset all tables before testing."""

if "SAUCE_USERNAME" in os.environ:
# Configure driver for Sauce Labs
# Presumes tunnel setup by Sauce Connect
# On TravisCI, Sauce Connect tunnel setup by Sauce Labs addon
# https://docs.travis-ci.com/user/sauce-connect
platform = {
"browserName": "firefox",
"platform": "Windows 10",
"version": "60.0",
}
capabilities = {
"tunnel-identifier": os.environ["TRAVIS_JOB_NUMBER"],
"extendedDebugging": "true",
}
metadata = {
"name": self.id(),
"build": "#%s %s" % (
os.environ["TRAVIS_BUILD_NUMBER"],
os.environ["TRAVIS_BRANCH"],
),
"tags": [
"py" + os.environ["TRAVIS_PYTHON_VERSION"],
"CI",
],
"passed": False,
}
capabilities.update(platform)
capabilities.update(metadata)

url = "http://{user}:{access_key}@localhost:4445/wd/hub".format(
user=os.environ["SAUCE_USERNAME"],
access_key=os.environ["SAUCE_ACCESS_KEY"],
)

self.driver = webdriver.Remote(
desired_capabilities=capabilities,
command_executor=url
)

else:
if "DISPLAY" not in os.environ:
# Non-graphical environment; use xvfb
self.xvfb = xvfbwrapper.Xvfb()
self.addCleanup(self.xvfb.stop)
self.xvfb.start()
self.driver = webdriver.Firefox(timeout=60)

self.addCleanup(self.driver.quit)

self.driver.root_uri = self.get_server_url()
self.driver.implicitly_wait(30)
self.verificationErrors = []
# default explicit wait time; use with Expected Conditions as needed
self.wait = WebDriverWait(self.driver, 60)
self.accept_next_alert = True

super(IntegrationTestCase, self).setUp()

def is_element_present(self, how, what):
"""Detects whether or not an element can be found in DOM

This function was exported from Selenium IDE
"""
try:
self.driver.find_element(by=how, value=what)
except NoSuchElementException as e:
return False
return True

def is_alert_present(self):
"""Detects whether an alert message is present

This function was exported from Selenium IDE
"""
try:
self.driver.switch_to_alert()
except NoAlertPresentException as e:
return False
return True

def close_alert_and_get_its_text(self):
"""Closes an alert, if present, and returns its text

If an alert is not present a NoAlertPresentException
will be thrown.
This function was exported from Selenium IDE
"""
try:
alert = self.driver.switch_to_alert()
alert_text = alert.text
if self.accept_next_alert:
alert.accept()
else:
alert.dismiss()
return alert_text
finally:
self.accept_next_alert = True

def tearDown(self):
"""Clean db session, drop all tables."""

# Update job result metadata on Sauce Labs, if available
if (
"SAUCE_USERNAME" in os.environ and

# No exception being handled - test completed successfully
sys.exc_info() == (None, None, None)
):
self.driver.execute_script("sauce:job-result=passed")

self.assertEqual([], self.verificationErrors)

super(IntegrationTestCase, self).tearDown()
8 changes: 8 additions & 0 deletions tests/integration_tests/pages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from page_objects import PageElement, PageObject


class LoginPage(PageObject):
username = PageElement(name='email')
password = PageElement(name='password')
login_button = PageElement(css='input[type="submit"]')
facebook_button = PageElement(css='.btn-facebook')
64 changes: 64 additions & 0 deletions tests/integration_tests/test_login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from flask import url_for
import pytest
from selenium.webdriver.support.ui import Select

from tests import DEFAULT_PASSWORD, TEST_USERNAME
from tests.integration_tests import IntegrationTestCase


class TestLogin(IntegrationTestCase):
"""Test class for Login integration tests"""

def test_login_page(self):
"""Ensure login works properly"""
driver = self.driver
driver.get(url_for("user.login", _external=True))

driver.find_element_by_name("email").click()
driver.find_element_by_name("email").clear()
driver.find_element_by_name("email").send_keys(TEST_USERNAME)
driver.find_element_by_name("password").click()
driver.find_element_by_name("password").clear()
driver.find_element_by_name("password").send_keys(DEFAULT_PASSWORD)
driver.find_element_by_xpath(
"//input[@class='btn btn-tnth-primary btn-lg' and @value='LOG IN']"
).click()
driver.find_element_by_id("tnthUserBtn").click()
driver.find_element_by_link_text("Log Out of TrueNTH").click()

@pytest.mark.skip(reason="not able to complete consent page")
def test_consent_after_login(self):
driver = self.driver
driver.get(url_for("eproms.home", _external=True))
driver.find_element_by_id("email").click()
driver.find_element_by_id("email").clear()
driver.find_element_by_id("email").send_keys(TEST_USERNAME)
driver.find_element_by_id("password").click()
driver.find_element_by_id("password").clear()
driver.find_element_by_id("password").send_keys(DEFAULT_PASSWORD)
driver.find_element_by_id("btnLogin").click()
driver.find_element_by_xpath("(.//*[normalize-space(text()) and normalize-space(.)='General Terms of Use'])[1]/following::i[1]").click()
driver.find_element_by_xpath("(.//*[normalize-space(text()) and normalize-space(.)='General Terms of Use'])[1]/following::i[2]").click()
driver.find_element_by_id("next").click()
driver.find_element_by_id("firstname").clear()
driver.find_element_by_id("firstname").send_keys("Test")
driver.find_element_by_id("lastname").click()
driver.find_element_by_id("lastname").clear()
driver.find_element_by_id("lastname").send_keys("User")
driver.find_element_by_id("date").click()
driver.find_element_by_id("month").click()
driver.find_element_by_xpath("(.//*[normalize-space(text()) and normalize-space(.)='(optional)'])[1]/following::option[11]").click()
driver.find_element_by_id("year").click()
driver.find_element_by_id("year").clear()
driver.find_element_by_id("year").send_keys("1988")
driver.find_element_by_id("role_patient").click()
driver.find_element_by_id("next").click()
driver.find_element_by_id("biopsy_no").click()
driver.find_element_by_id("next").click()
driver.find_element_by_id("stateSelector").click()
Select(driver.find_element_by_id("stateSelector")).select_by_visible_text("Washington")
driver.find_element_by_xpath("(.//*[normalize-space(text()) and normalize-space(.)='Your clinic of care.'])[1]/following::option[12]").click()
driver.find_element_by_xpath("(.//*[normalize-space(text()) and normalize-space(.)='UW Medicine (University of Washington)'])[1]/following::label[1]").click()
driver.find_element_by_id("updateProfile").click()
driver.find_element_by_id("tnthUserBtn").click()
driver.find_element_by_link_text("Log Out of TrueNTH").click()
Loading