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

Set jobs display to own #216

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
19 changes: 14 additions & 5 deletions clockwork_frontend_test/test_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,20 @@
DASHBOARD_TABLE_CONTENT = []
for job in fake_data["jobs"]:
if job["cw"]["mila_email_username"] == "[email protected]":
# This element could be an array of states, or a simple string.
# For now, each array we encountered contained only one element.
job_states = job["slurm"]["job_state"]

DASHBOARD_TABLE_CONTENT.append(
[
job["slurm"]["cluster_name"],
int(
job["slurm"]["job_id"]
), # job ID is currently handled as a numeric value
job["slurm"]["name"],
job["slurm"]["job_state"].lower(),
job_states[0].lower()
if isinstance(job_states, list)
else job_states.lower(),
get_default_display_date(job["slurm"]["submit_time"]),
get_default_display_date(job["slurm"]["start_time"]),
get_default_display_date(job["slurm"]["end_time"]),
Expand Down Expand Up @@ -180,10 +186,10 @@ def _check_dashboard_table_sorting(
header = headers.nth(column_id)
expect(header).to_contain_text(column_text)
header.click()
_check_dashboard_table(page, content)
_check_dashboard_table(page, content, column_id=column_id)


def _check_dashboard_table(page: Page, table_content: list):
def _check_dashboard_table(page: Page, table_content: list, column_id: int = None):
"""Check dashboard table contains expected table content.

table_content is a list or rows, each row is a list of texts expected in related columns.
Expand All @@ -195,5 +201,8 @@ def _check_dashboard_table(page: Page, table_content: list):
for index_row, content_row in enumerate(table_content):
cols = rows.nth(index_row).locator("td")
expect(cols).to_have_count(8)
for index_col, content_col in enumerate(content_row):
expect(cols.nth(index_col)).to_contain_text(str(content_col))
if column_id is None:
for index_col, content_col in enumerate(content_row):
expect(cols.nth(index_col)).to_contain_text(str(content_col))
else:
expect(cols.nth(column_id)).to_contain_text(str(content_row[column_id]))
5 changes: 3 additions & 2 deletions clockwork_frontend_test/test_jobs_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from random import choice

from clockwork_frontend_test.utils import BASE_URL, get_fake_data
from clockwork_web.core.jobs_helper import get_inferred_job_state
from clockwork_web.core.jobs_helper import get_inferred_job_state, get_str_job_state

# Retrieve data we are interested in from the fake data
fake_data = get_fake_data()
Expand Down Expand Up @@ -388,7 +388,8 @@ def test_filter_by_status_except_one(page: Page):
job["slurm"]["job_id"],
]
for job in sorted_jobs
if get_inferred_job_state(job["slurm"]["job_state"]) != "RUNNING"
if get_inferred_job_state(get_str_job_state(job["slurm"]["job_state"]))
!= "RUNNING"
][:40]

_check_jobs_table(
Expand Down
7 changes: 4 additions & 3 deletions clockwork_frontend_test/test_jobs_search_for_student06.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from playwright.sync_api import Page, expect

from clockwork_frontend_test.utils import BASE_URL, get_fake_data
from clockwork_web.core.jobs_helper import get_inferred_job_state
from clockwork_web.core.jobs_helper import get_inferred_job_state, get_str_job_state

current_username = "[email protected]"

Expand Down Expand Up @@ -283,7 +283,8 @@ def test_filter_by_status_except_one(page: Page):
job["slurm"]["job_id"],
]
for job in sorted_mila_jobs
if "RUNNING" != get_inferred_job_state(job["slurm"]["job_state"])
if "RUNNING"
!= get_inferred_job_state(get_str_job_state(job["slurm"]["job_state"]))
][:40]
_check_jobs_table(
page,
Expand Down Expand Up @@ -419,7 +420,7 @@ def test_multiple_filters(page: Page):
]
for job in sorted_jobs
if job["cw"]["mila_email_username"] == "[email protected]"
and get_inferred_job_state(job["slurm"]["job_state"])
and get_inferred_job_state(get_str_job_state(job["slurm"]["job_state"]))
in ["COMPLETED", "PENDING", "FAILED"]
][:40]
_check_jobs_table(
Expand Down
64 changes: 41 additions & 23 deletions clockwork_frontend_test/test_settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from playwright.sync_api import Page, expect
import math

from clockwork_frontend_test.utils import BASE_URL
from clockwork_frontend_test.utils import BASE_URL, get_fake_data


def test_languages(page: Page):
Expand Down Expand Up @@ -42,17 +43,41 @@ def test_nb_items_per_page(page: Page):
page.goto(f"{BASE_URL}/jobs/search")
# Check we have 40 rows by default in table.
rows = page.locator("table#search_table tbody tr")
expect(rows).to_have_count(40)
# Check we have 3 pages in table nav.
nav_elements = page.locator("nav.table_nav ul.pagination li.page-item")
expect(nav_elements).to_have_count(6)
expect(nav_elements.nth(0)).to_have_class("page-item first")
expect(nav_elements.nth(1)).to_have_class("page-item current")
expect(nav_elements.nth(1)).to_have_text("1")
expect(nav_elements.nth(2)).to_have_text("2")
expect(nav_elements.nth(3)).to_have_text("3")
expect(nav_elements.nth(4)).to_have_class("page-item last")
expect(nav_elements.nth(5)).to_have_class("page-item last")
nb_jobs_per_page = 40
expect(rows).to_have_count(nb_jobs_per_page)

# Get the number of jobs to display
fake_data = get_fake_data()
nb_jobs = len(fake_data["jobs"])
# Check how should the table nav look
nb_pages = math.floor(nb_jobs / nb_jobs_per_page) + 1

# Check we have X pages in table nav.
def _check_nav_table(nb_pages):
if nb_pages < 4:
nav_elements = page.locator("nav.table_nav ul.pagination li.page-item")
expect(nav_elements).to_have_count(6)
expect(nav_elements.nth(0)).to_have_class("page-item first")
expect(nav_elements.nth(1)).to_have_class("page-item current")
expect(nav_elements.nth(1)).to_have_text("1")
expect(nav_elements.nth(2)).to_have_text("2")
expect(nav_elements.nth(3)).to_have_text("3")
expect(nav_elements.nth(4)).to_have_class("page-item last")
expect(nav_elements.nth(5)).to_have_class("page-item last")
else:
nav_elements = page.locator("nav.table_nav ul.pagination li")
expect(nav_elements).to_have_count(8)
expect(nav_elements.nth(0)).to_have_class("page-item first")
expect(nav_elements.nth(1)).to_have_class("page-item current")
expect(nav_elements.nth(1)).to_have_text("1")
expect(nav_elements.nth(2)).to_have_text("2")
expect(nav_elements.nth(3)).to_have_text("3")
expect(nav_elements.nth(4)).to_have_text("4")
expect(nav_elements.nth(5)).to_have_text("...")
expect(nav_elements.nth(6)).to_have_class("page-item last")
expect(nav_elements.nth(7)).to_have_class("page-item last")

_check_nav_table(nb_pages)

# Go to settings.
page.goto(f"{BASE_URL}/settings/")
Expand All @@ -67,17 +92,10 @@ def test_nb_items_per_page(page: Page):
page.goto(f"{BASE_URL}/jobs/search")
rows = page.locator("table#search_table tbody tr")
expect(rows).to_have_count(25)
nav_elements = page.locator("nav.table_nav ul.pagination li")
expect(nav_elements).to_have_count(8)
expect(nav_elements.nth(0)).to_have_class("page-item first")
expect(nav_elements.nth(1)).to_have_class("page-item current")
expect(nav_elements.nth(1)).to_have_text("1")
expect(nav_elements.nth(2)).to_have_text("2")
expect(nav_elements.nth(3)).to_have_text("3")
expect(nav_elements.nth(4)).to_have_text("4")
expect(nav_elements.nth(5)).to_have_text("...")
expect(nav_elements.nth(6)).to_have_class("page-item last")
expect(nav_elements.nth(7)).to_have_class("page-item last")

nb_jobs_per_page = 25
nb_pages = math.floor(nb_jobs / nb_jobs_per_page) + 1
_check_nav_table(nb_pages)

# Move back to 40 jobs per page.
page.goto(f"{BASE_URL}/settings/")
Expand Down
28 changes: 28 additions & 0 deletions clockwork_tools_test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,29 @@ def invalid_config_00():
return config


@pytest.fixture
def admin_config(fake_data):
"""
The configuration for an admin, who can access all jobs
"""
for user in fake_data["users"]:
if (
"admin_access" in user and user["admin_access"]
): # Fake data should always contain an admin
email = user["mila_email_username"]
clockwork_api_key = user["clockwork_api_key"]
break

config = {
"host": os.environ["clockwork_tools_test_HOST"],
"port": os.environ["clockwork_tools_test_PORT"],
"email": email,
"clockwork_api_key": clockwork_api_key,
}

return config


@pytest.fixture
def invalid_config_01():
"""
Expand Down Expand Up @@ -81,6 +104,11 @@ def mtclient(config, db_with_fake_data):
return clockwork_tools.client.ClockworkToolsClient(**config)


@pytest.fixture
def mtclient_admin(admin_config, db_with_fake_data):
return clockwork_tools.client.ClockworkToolsClient(**admin_config)


@pytest.fixture
def unauthorized_mtclient_00(invalid_config_00, db_with_fake_data):
return clockwork_tools.client.ClockworkToolsClient(**invalid_config_00)
Expand Down
20 changes: 10 additions & 10 deletions clockwork_tools_test/test_mt_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,49 +11,49 @@
@pytest.mark.parametrize(
"cluster_name", ("mila", "cedar", "graham", "beluga", "sephiroth")
)
def test_jobs_list_with_filter(mtclient, fake_data, cluster_name):
def test_jobs_list_with_filter(mtclient_admin, fake_data, cluster_name):
"""
Test the `jobs_list` command. This is just to make sure that the filtering works
and the `cluster_name` argument is functional.

Note that "sephiroth" is not a valid cluster, so we will expect empty lists as results.
"""
validator = helper_jobs_list_with_filter(fake_data, cluster_name=cluster_name)
LD_jobs = mtclient.jobs_list(cluster_name=cluster_name)
LD_jobs = mtclient_admin.jobs_list(cluster_name=cluster_name)
validator(LD_jobs)


@pytest.mark.parametrize("username", ("yoshi", "koopatroopa"))
def test_api_list_invalid_username(mtclient, username):
def test_api_list_invalid_username(mtclient_admin, username):
""" """
LD_retrieved_jobs = mtclient.jobs_list(username=username)
LD_retrieved_jobs = mtclient_admin.jobs_list(username=username)
# we expect no matches for those made-up names
assert len(LD_retrieved_jobs) == 0


@pytest.mark.parametrize("cluster_name", ("mila", "beluga", "cedar", "graham"))
def test_single_job_at_random(mtclient, fake_data, cluster_name):
def test_single_job_at_random(mtclient_admin, fake_data, cluster_name):
"""
This job entry should be present in the database.
"""
validator, job_id = helper_single_job_at_random(fake_data, cluster_name)
D_job = mtclient.jobs_one(job_id=job_id)
D_job = mtclient_admin.jobs_one(job_id=job_id)
validator(D_job)


def test_single_job_missing(mtclient, fake_data):
def test_single_job_missing(mtclient_admin, fake_data):
"""
This job entry should be missing from the database.
"""
validator, job_id = helper_single_job_missing(fake_data)
D_job = mtclient.jobs_one(job_id=job_id)
D_job = mtclient_admin.jobs_one(job_id=job_id)
validator(D_job)


def test_list_jobs_for_a_given_random_user(mtclient, fake_data):
def test_list_jobs_for_a_given_random_user(mtclient_admin, fake_data):
"""
Verify that we list the jobs properly for a given random user.
"""
validator, username = helper_list_jobs_for_a_given_random_user(fake_data)
LD_jobs = mtclient.jobs_list(username=username)
LD_jobs = mtclient_admin.jobs_list(username=username)
validator(LD_jobs)
12 changes: 11 additions & 1 deletion clockwork_web/core/jobs_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ def get_filter_after_end_time(end_time):
}


def get_str_job_state(job_state):
"""
Handle the different job state formats we retrieve accross the different Slurm versions
"""
if isinstance(job_state, list) and len(job_state) > 0:
return job_state[0]

return job_state


def combine_all_mongodb_filters(*mongodb_filters):
"""
Creates a big AND clause if more than one argument is given.
Expand Down Expand Up @@ -442,7 +452,7 @@ def get_inferred_job_state(job_state):

Returns the associated Clockwork job state
"""
return job_state_to_aggregated[job_state.upper()]
return job_state_to_aggregated[get_str_job_state(job_state).upper()]


def get_jobs_properties_list_per_page():
Expand Down
6 changes: 5 additions & 1 deletion clockwork_web/core/search_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,13 @@ def parse_search_request(user, args, force_pagination=True):
def search_request(user, args, force_pagination=True):
query = parse_search_request(user, args, force_pagination=force_pagination)

username = None
if not user.is_admin() and query.username != user.get_id():
return (query, [], 0) # A non-admin user can not see other user's jobs

# Call a helper to retrieve the jobs
(jobs, nbr_total_jobs) = get_jobs(
username=query.username,
username=query.username if user.is_admin() else user.mila_email_username,
cluster_names=query.cluster_name,
job_states=query.job_state,
job_ids=query.job_ids,
Expand Down
9 changes: 8 additions & 1 deletion clockwork_web/rest_routes/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,15 @@ def route_api_v1_jobs_one():
if len(cluster_names) < 1:
return jsonify({}), 200

# If the current user is not an admin, we only retrieve his/her jobs
username = None
if not current_user.is_admin():
username = current_user.mila_email_username

# Set up the filters and retrieve the expected job
(LD_jobs, _) = get_jobs(job_ids=[job_id], cluster_names=cluster_names)
(LD_jobs, _) = get_jobs(
username=username, job_ids=[job_id], cluster_names=cluster_names
)

if len(LD_jobs) == 0:
# Not a great when missing the value we want, but it's an acceptable answer.
Expand Down
2 changes: 1 addition & 1 deletion clockwork_web/templates/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
},
"job_id": {
"label": "{{ gettext('Job ID') }}",
"sortable": "alphabetically"
"sortable": "numeric"
},
"job_name": {
"label": "{{ gettext('Job name [:20]') }}",
Expand Down
6 changes: 6 additions & 0 deletions clockwork_web/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ def boolean(value):
def get_id(self):
return self.mila_email_username

def is_admin(self):
return self.admin_access

@staticmethod
def get(mila_email_username: str):
"""
Expand Down Expand Up @@ -349,3 +352,6 @@ def get_web_settings(self):
A dictionary presenting the default web settings.
"""
return self.web_settings

def is_admin(self):
return False
13 changes: 13 additions & 0 deletions clockwork_web_test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,19 @@ def valid_rest_auth_headers():
return {"Authorization": f"Basic {encoded_s}"}


@pytest.fixture
def valid_admin_rest_auth_headers(fake_data):
for user in fake_data["users"]:
if "admin_access" in user and user["admin_access"]:
username = user["mila_email_username"]
api_key = user["clockwork_api_key"]

s = f"{username}:{api_key}"
encoded_bytes = base64.b64encode(s.encode("utf-8"))
encoded_s = str(encoded_bytes, "utf-8")
return {"Authorization": f"Basic {encoded_s}"}


@pytest.fixture
def known_user(app, fake_data):
# Assert that the users of the fake data exist and are not empty
Expand Down
Loading
Loading