diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts index 93d99b75fa0a..0b3550f4bbf8 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -8753,6 +8753,11 @@ export interface components { * @description The email of the user that owns this job. Only the owner of the job and administrators can see this value. */ user_email?: string | null; + /** + * User Id + * @description User ID of user that ran this job + */ + user_id?: string | null; }; /** EncodedJobParameterHistoryItem */ EncodedJobParameterHistoryItem: { @@ -16177,6 +16182,11 @@ export interface components { * @description The email of the user that owns this job. Only the owner of the job and administrators can see this value. */ user_email?: string | null; + /** + * User Id + * @description User ID of user that ran this job + */ + user_id?: string | null; }; /** * Src diff --git a/lib/galaxy/managers/jobs.py b/lib/galaxy/managers/jobs.py index 1d4a61dac6f0..4b214f5b8af8 100644 --- a/lib/galaxy/managers/jobs.py +++ b/lib/galaxy/managers/jobs.py @@ -493,11 +493,18 @@ def _build_job_subquery( self, tool_id: str, user_id: int, tool_version: Optional[str], job_state, wildcard_param_dump ): """Build subquery that selects a job with correct job parameters.""" - stmt = select(model.Job.id).where( - and_( - model.Job.tool_id == tool_id, - model.Job.user_id == user_id, - model.Job.copied_from_job_id.is_(None), # Always pick original job + stmt = ( + select(model.Job.id) + .join(model.History, model.Job.history_id == model.History.id) + .where( + and_( + model.Job.tool_id == tool_id, + or_( + model.Job.user_id == user_id, + model.History.published == true(), + ), + model.Job.copied_from_job_id.is_(None), # Always pick original job + ) ) ) if tool_version: diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index ad657b8cc717..f1015a284738 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -1549,6 +1549,7 @@ class Job(Base, JobLike, UsesCreateAndUpdateTime, Dictifiable, Serializable): "galaxy_version", "command_version", "copied_from_job_id", + "user_id", ] _numeric_metric = JobMetricNumeric diff --git a/lib/galaxy/schema/jobs.py b/lib/galaxy/schema/jobs.py index ec7a971a3614..44cc5061619c 100644 --- a/lib/galaxy/schema/jobs.py +++ b/lib/galaxy/schema/jobs.py @@ -191,6 +191,7 @@ class EncodedJobDetails(JobSummary): title="Output collections", description="", ) + user_id: Optional[EncodedDatabaseIdField] = Field(default=None, description="User ID of user that ran this job") class JobDestinationParams(Model): diff --git a/lib/galaxy_test/api/test_tools.py b/lib/galaxy_test/api/test_tools.py index 75c3998d8f30..6be4ea256490 100644 --- a/lib/galaxy_test/api/test_tools.py +++ b/lib/galaxy_test/api/test_tools.py @@ -8,7 +8,9 @@ Any, Dict, List, + Optional, ) +from uuid import uuid4 import pytest from requests import ( @@ -974,15 +976,19 @@ def test_run_cat1(self): output1_content = self.dataset_populator.get_history_dataset_content(history_id, dataset=output1) assert output1_content.strip() == "Cat1Test" + def _get_cat1_inputs(self, history_id): + new_dataset = self.dataset_populator.new_dataset(history_id, content="Cat1Test") + inputs = dict( + input1=dataset_to_param(new_dataset), + ) + return inputs + @skip_without_tool("cat1") @requires_new_history def test_run_cat1_use_cached_job(self): with self.dataset_populator.test_history_for(self.test_run_cat1_use_cached_job) as history_id: # Run simple non-upload tool with an input data parameter. - new_dataset = self.dataset_populator.new_dataset(history_id, content="Cat1Test") - inputs = dict( - input1=dataset_to_param(new_dataset), - ) + inputs = self._get_cat1_inputs(history_id) outputs_one = self._run_cat1(history_id, inputs=inputs, assert_ok=True, wait_for_job=True) outputs_two = self._run_cat1( history_id, inputs=inputs, use_cached_job=False, assert_ok=True, wait_for_job=True @@ -998,6 +1004,31 @@ def test_run_cat1_use_cached_job(self): assert len(filenames) == 3, filenames assert len(set(filenames)) <= 2, filenames + @skip_without_tool("cat1") + @requires_new_history + def test_run_cat1_use_cached_job_from_public_history(self): + with self.dataset_populator.test_history_for(self.test_run_cat1_use_cached_job) as history_id: + # Run simple non-upload tool with an input data parameter. + inputs = self._get_cat1_inputs(history_id) + original_output = self._run_cat1(history_id, inputs=inputs, assert_ok=True, wait_for_job=True) + original_job = self.dataset_populator.get_job_details(original_output["jobs"][0]["id"], full=True).json() + + def run_again(user_email): + with self._different_user_and_history(user_email=user_email) as different_history_id: + cached_output = self._run_cat1( + different_history_id, inputs=inputs, use_cached_job=True, assert_ok=True, wait_for_job=True + ) + return self.dataset_populator.get_job_details(cached_output["jobs"][0]["id"], full=True).json() + + job = run_again(f"{uuid4()}@test.com") + assert job["user_id"] != original_job["user_id"] + assert not job["copied_from_job_id"] + # publish history, now we can use cached job + self.dataset_populator.make_public(history_id=history_id) + cached_job = run_again(f"{uuid4()}@test.com") + assert cached_job["user_id"] != original_job["user_id"] + assert cached_job["copied_from_job_id"] == original_output["jobs"][0]["id"] + @skip_without_tool("cat1") def test_run_cat1_listified_param(self): with self.dataset_populator.test_history_for(self.test_run_cat1_listified_param) as history_id: @@ -2809,8 +2840,8 @@ def _assert_dataset_permission_denied_response(self, response): # assert "User does not have permission to use a dataset" in err_message, err_message @contextlib.contextmanager - def _different_user_and_history(self): - with self._different_user(): + def _different_user_and_history(self, user_email: Optional[str] = None): + with self._different_user(email=user_email): with self.dataset_populator.test_history() as other_history_id: yield other_history_id diff --git a/lib/galaxy_test/base/api.py b/lib/galaxy_test/base/api.py index 63e33dd5e90a..b1881f917b63 100644 --- a/lib/galaxy_test/base/api.py +++ b/lib/galaxy_test/base/api.py @@ -147,7 +147,7 @@ def _setup_user_get_key(self, email, password=None): return user, self._post(f"users/{user['id']}/api_key", admin=True).json() @contextmanager - def _different_user(self, email=OTHER_USER, anon=False): + def _different_user(self, email: Optional[str] = None, anon=False): """Use in test cases to switch get/post operations to act as new user ..code-block:: python @@ -164,6 +164,7 @@ def _different_user(self, email=OTHER_USER, anon=False): self.galaxy_interactor.cookies = cookies new_key = None else: + email = OTHER_USER if email is None else email _, new_key = self._setup_user_get_key(email) try: self.user_api_key = new_key