mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge branch 'release_24.0' into dev
This commit is contained in:
@@ -7,11 +7,19 @@ import { components } from "@/api/schema";
|
||||
*/
|
||||
export type HistorySummary = components["schemas"]["HistorySummary"];
|
||||
|
||||
export interface HistorySummaryExtended extends HistorySummary {
|
||||
size: number;
|
||||
contents_active: components["schemas"]["HistoryActiveContentCounts"];
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains additional details about a History.
|
||||
*/
|
||||
export type HistoryDetailed = components["schemas"]["HistoryDetailed"];
|
||||
|
||||
export type AnyHistory = HistorySummary | HistorySummaryExtended | HistoryDetailed;
|
||||
|
||||
/**
|
||||
* Contains minimal information about a HistoryContentItem.
|
||||
*/
|
||||
@@ -125,3 +133,46 @@ export function hasDetails(entry: DatasetEntry): entry is DatasetDetails {
|
||||
* Contains dataset metadata information.
|
||||
*/
|
||||
export type MetadataFiles = components["schemas"]["MetadataFile"][];
|
||||
|
||||
export function isHistorySummary(history: AnyHistory): history is HistorySummary {
|
||||
return !("user_id" in history);
|
||||
}
|
||||
|
||||
export function isHistorySummaryExtended(history: AnyHistory): history is HistorySummaryExtended {
|
||||
return "contents_active" in history && "user_id" in history;
|
||||
}
|
||||
|
||||
type QuotaUsageResponse = components["schemas"]["UserQuotaUsage"];
|
||||
|
||||
export interface User extends QuotaUsageResponse {
|
||||
id: string;
|
||||
email: string;
|
||||
tags_used: string[];
|
||||
isAnonymous: false;
|
||||
is_admin?: boolean;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface AnonymousUser {
|
||||
isAnonymous: true;
|
||||
username?: string;
|
||||
is_admin?: false;
|
||||
}
|
||||
|
||||
export type GenericUser = User | AnonymousUser;
|
||||
|
||||
export function isRegisteredUser(user: User | AnonymousUser | null): user is User {
|
||||
return !user?.isAnonymous;
|
||||
}
|
||||
|
||||
export function userOwnsHistory(user: User | AnonymousUser | null, history: AnyHistory) {
|
||||
return (
|
||||
// Assuming histories without user_id are owned by the current user
|
||||
(isRegisteredUser(user) && !hasOwner(history)) ||
|
||||
(isRegisteredUser(user) && hasOwner(history) && user.id === history.user_id)
|
||||
);
|
||||
}
|
||||
|
||||
function hasOwner(history: AnyHistory): history is HistorySummaryExtended {
|
||||
return "user_id" in history;
|
||||
}
|
||||
|
||||
+190
-46
@@ -2336,7 +2336,6 @@ export interface components {
|
||||
* @description The relative URL in the form of /u/{username}/h/{slug}
|
||||
*/
|
||||
username_and_slug?: string | null;
|
||||
[key: string]: unknown | undefined;
|
||||
};
|
||||
/** ArchivedHistorySummary */
|
||||
ArchivedHistorySummary: {
|
||||
@@ -2409,7 +2408,6 @@ export interface components {
|
||||
* @description The relative URL to access this item.
|
||||
*/
|
||||
url: string;
|
||||
[key: string]: unknown | undefined;
|
||||
};
|
||||
/** AsyncFile */
|
||||
AsyncFile: {
|
||||
@@ -3597,6 +3595,153 @@ export interface components {
|
||||
CustomHistoryItem: {
|
||||
[key: string]: unknown | undefined;
|
||||
};
|
||||
/** CustomHistoryView */
|
||||
CustomHistoryView: {
|
||||
/**
|
||||
* Annotation
|
||||
* @description An annotation to provide details or to help understand the purpose and usage of this item.
|
||||
*/
|
||||
annotation?: string | null;
|
||||
/**
|
||||
* Archived
|
||||
* @description Whether this item has been archived and is no longer active.
|
||||
*/
|
||||
archived?: boolean | null;
|
||||
/**
|
||||
* Contents Active
|
||||
* @description Contains the number of active, deleted or hidden items in a History.
|
||||
*/
|
||||
contents_active?: components["schemas"]["HistoryActiveContentCounts"] | null;
|
||||
/**
|
||||
* Contents States
|
||||
* @description A dictionary keyed to possible dataset states and valued with the number of datasets in this history that have those states.
|
||||
*/
|
||||
contents_states?: {
|
||||
[key: string]: number | undefined;
|
||||
} | null;
|
||||
/**
|
||||
* Contents URL
|
||||
* @description The relative URL to access the contents of this History.
|
||||
*/
|
||||
contents_url?: string | null;
|
||||
/**
|
||||
* Count
|
||||
* @description The number of items in the history.
|
||||
*/
|
||||
count?: number | null;
|
||||
/**
|
||||
* Create Time
|
||||
* @description The time and date this item was created.
|
||||
*/
|
||||
create_time?: string | null;
|
||||
/**
|
||||
* Deleted
|
||||
* @description Whether this item is marked as deleted.
|
||||
*/
|
||||
deleted?: boolean | null;
|
||||
/**
|
||||
* Genome Build
|
||||
* @description TODO
|
||||
*/
|
||||
genome_build?: string | null;
|
||||
/**
|
||||
* History ID
|
||||
* @example 0123456789ABCDEF
|
||||
*/
|
||||
id?: string;
|
||||
/**
|
||||
* Importable
|
||||
* @description Whether this History can be imported by other users with a shared link.
|
||||
*/
|
||||
importable?: boolean | null;
|
||||
/**
|
||||
* Model class
|
||||
* @description The name of the database model class.
|
||||
* @constant
|
||||
*/
|
||||
model_class?: "History";
|
||||
/**
|
||||
* Name
|
||||
* @description The name of the history.
|
||||
*/
|
||||
name?: string | null;
|
||||
/**
|
||||
* Nice Size
|
||||
* @description The total size of the contents of this history in a human-readable format.
|
||||
*/
|
||||
nice_size?: string | null;
|
||||
/**
|
||||
* Preferred Object Store ID
|
||||
* @description The ID of the object store that should be used to store new datasets in this history.
|
||||
*/
|
||||
preferred_object_store_id?: string | null;
|
||||
/**
|
||||
* Published
|
||||
* @description Whether this resource is currently publicly available to all users.
|
||||
*/
|
||||
published?: boolean | null;
|
||||
/**
|
||||
* Purged
|
||||
* @description Whether this item has been permanently removed.
|
||||
*/
|
||||
purged?: boolean | null;
|
||||
/**
|
||||
* Size
|
||||
* @description The total size of the contents of this history in bytes.
|
||||
*/
|
||||
size?: number | null;
|
||||
/**
|
||||
* Slug
|
||||
* @description Part of the URL to uniquely identify this History by link in a readable way.
|
||||
*/
|
||||
slug?: string | null;
|
||||
/**
|
||||
* State
|
||||
* @description The current state of the History based on the states of the datasets it contains.
|
||||
*/
|
||||
state?: components["schemas"]["DatasetState"] | null;
|
||||
/**
|
||||
* State Counts
|
||||
* @description A dictionary keyed to possible dataset states and valued with the number of datasets in this history that have those states.
|
||||
*/
|
||||
state_details?: {
|
||||
[key: string]: number | undefined;
|
||||
} | null;
|
||||
/**
|
||||
* State IDs
|
||||
* @description A dictionary keyed to possible dataset states and valued with lists containing the ids of each HDA in that state.
|
||||
*/
|
||||
state_ids?: {
|
||||
[key: string]: string[] | undefined;
|
||||
} | null;
|
||||
tags?: components["schemas"]["TagCollection"] | null;
|
||||
/**
|
||||
* Update Time
|
||||
* @description The last time and date this item was updated.
|
||||
*/
|
||||
update_time?: string | null;
|
||||
/**
|
||||
* URL
|
||||
* @deprecated
|
||||
* @description The relative URL to access this item.
|
||||
*/
|
||||
url?: string | null;
|
||||
/**
|
||||
* User ID
|
||||
* @description The encoded ID of the user that owns this History.
|
||||
*/
|
||||
user_id?: string | null;
|
||||
/**
|
||||
* Username
|
||||
* @description Owner of the history
|
||||
*/
|
||||
username?: string | null;
|
||||
/**
|
||||
* Username and slug
|
||||
* @description The relative URL in the form of /u/{username}/h/{slug}
|
||||
*/
|
||||
username_and_slug?: string | null;
|
||||
};
|
||||
/**
|
||||
* DCESummary
|
||||
* @description Dataset Collection Element summary information.
|
||||
@@ -6378,6 +6523,27 @@ export interface components {
|
||||
HelpForumUser: {
|
||||
[key: string]: unknown | undefined;
|
||||
};
|
||||
/**
|
||||
* HistoryActiveContentCounts
|
||||
* @description Contains the number of active, deleted or hidden items in a History.
|
||||
*/
|
||||
HistoryActiveContentCounts: {
|
||||
/**
|
||||
* Active
|
||||
* @description Number of active datasets.
|
||||
*/
|
||||
active: number;
|
||||
/**
|
||||
* Deleted
|
||||
* @description Number of deleted datasets.
|
||||
*/
|
||||
deleted: number;
|
||||
/**
|
||||
* Hidden
|
||||
* @description Number of hidden datasets.
|
||||
*/
|
||||
hidden: number;
|
||||
};
|
||||
/** HistoryContentBulkOperationPayload */
|
||||
HistoryContentBulkOperationPayload: {
|
||||
/** Items */
|
||||
@@ -6612,27 +6778,6 @@ export interface components {
|
||||
* @description The relative URL in the form of /u/{username}/h/{slug}
|
||||
*/
|
||||
username_and_slug?: string | null;
|
||||
[key: string]: unknown | undefined;
|
||||
};
|
||||
/**
|
||||
* HistoryMinimal
|
||||
* @description Minimal History Response with optional fields
|
||||
*/
|
||||
HistoryMinimal: {
|
||||
/** Id */
|
||||
id?: string | null;
|
||||
/**
|
||||
* Model class
|
||||
* @description The name of the database model class.
|
||||
* @constant
|
||||
*/
|
||||
model_class: "History";
|
||||
/**
|
||||
* User ID
|
||||
* @description The encoded ID of the user that owns this History.
|
||||
*/
|
||||
user_id?: string | null;
|
||||
[key: string]: unknown | undefined;
|
||||
};
|
||||
/**
|
||||
* HistorySummary
|
||||
@@ -6703,7 +6848,6 @@ export interface components {
|
||||
* @description The relative URL to access this item.
|
||||
*/
|
||||
url: string;
|
||||
[key: string]: unknown | undefined;
|
||||
};
|
||||
/**
|
||||
* Hyperlink
|
||||
@@ -15139,9 +15283,9 @@ export interface operations {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": (
|
||||
| components["schemas"]["CustomHistoryView"]
|
||||
| components["schemas"]["HistoryDetailed"]
|
||||
| components["schemas"]["HistorySummary"]
|
||||
| components["schemas"]["HistoryMinimal"]
|
||||
)[];
|
||||
};
|
||||
};
|
||||
@@ -15181,9 +15325,9 @@ export interface operations {
|
||||
content: {
|
||||
"application/json":
|
||||
| components["schemas"]["JobImportHistoryResponse"]
|
||||
| components["schemas"]["CustomHistoryView"]
|
||||
| components["schemas"]["HistoryDetailed"]
|
||||
| components["schemas"]["HistorySummary"]
|
||||
| components["schemas"]["HistoryMinimal"];
|
||||
| components["schemas"]["HistorySummary"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
@@ -15267,9 +15411,9 @@ export interface operations {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": (
|
||||
| components["schemas"]["CustomHistoryView"]
|
||||
| components["schemas"]["HistoryDetailed"]
|
||||
| components["schemas"]["HistorySummary"]
|
||||
| components["schemas"]["HistoryMinimal"]
|
||||
)[];
|
||||
};
|
||||
};
|
||||
@@ -15305,9 +15449,9 @@ export interface operations {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": (
|
||||
| components["schemas"]["CustomHistoryView"]
|
||||
| components["schemas"]["HistoryDetailed"]
|
||||
| components["schemas"]["HistorySummary"]
|
||||
| components["schemas"]["HistoryMinimal"]
|
||||
)[];
|
||||
};
|
||||
};
|
||||
@@ -15373,9 +15517,9 @@ export interface operations {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": (
|
||||
| components["schemas"]["CustomHistoryView"]
|
||||
| components["schemas"]["HistoryDetailed"]
|
||||
| components["schemas"]["HistorySummary"]
|
||||
| components["schemas"]["HistoryMinimal"]
|
||||
)[];
|
||||
};
|
||||
};
|
||||
@@ -15410,9 +15554,9 @@ export interface operations {
|
||||
200: {
|
||||
content: {
|
||||
"application/json":
|
||||
| components["schemas"]["CustomHistoryView"]
|
||||
| components["schemas"]["HistoryDetailed"]
|
||||
| components["schemas"]["HistorySummary"]
|
||||
| components["schemas"]["HistoryMinimal"];
|
||||
| components["schemas"]["HistorySummary"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
@@ -15447,9 +15591,9 @@ export interface operations {
|
||||
200: {
|
||||
content: {
|
||||
"application/json":
|
||||
| components["schemas"]["CustomHistoryView"]
|
||||
| components["schemas"]["HistoryDetailed"]
|
||||
| components["schemas"]["HistorySummary"]
|
||||
| components["schemas"]["HistoryMinimal"];
|
||||
| components["schemas"]["HistorySummary"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
@@ -15507,9 +15651,9 @@ export interface operations {
|
||||
200: {
|
||||
content: {
|
||||
"application/json":
|
||||
| components["schemas"]["CustomHistoryView"]
|
||||
| components["schemas"]["HistoryDetailed"]
|
||||
| components["schemas"]["HistorySummary"]
|
||||
| components["schemas"]["HistoryMinimal"];
|
||||
| components["schemas"]["HistorySummary"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
@@ -15549,9 +15693,9 @@ export interface operations {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": (
|
||||
| components["schemas"]["CustomHistoryView"]
|
||||
| components["schemas"]["HistoryDetailed"]
|
||||
| components["schemas"]["HistorySummary"]
|
||||
| components["schemas"]["HistoryMinimal"]
|
||||
)[];
|
||||
};
|
||||
};
|
||||
@@ -15592,9 +15736,9 @@ export interface operations {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": (
|
||||
| components["schemas"]["CustomHistoryView"]
|
||||
| components["schemas"]["HistoryDetailed"]
|
||||
| components["schemas"]["HistorySummary"]
|
||||
| components["schemas"]["HistoryMinimal"]
|
||||
)[];
|
||||
};
|
||||
};
|
||||
@@ -15629,9 +15773,9 @@ export interface operations {
|
||||
200: {
|
||||
content: {
|
||||
"application/json":
|
||||
| components["schemas"]["CustomHistoryView"]
|
||||
| components["schemas"]["HistoryDetailed"]
|
||||
| components["schemas"]["HistorySummary"]
|
||||
| components["schemas"]["HistoryMinimal"];
|
||||
| components["schemas"]["HistorySummary"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
@@ -15670,9 +15814,9 @@ export interface operations {
|
||||
200: {
|
||||
content: {
|
||||
"application/json":
|
||||
| components["schemas"]["CustomHistoryView"]
|
||||
| components["schemas"]["HistoryDetailed"]
|
||||
| components["schemas"]["HistorySummary"]
|
||||
| components["schemas"]["HistoryMinimal"];
|
||||
| components["schemas"]["HistorySummary"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
@@ -15712,9 +15856,9 @@ export interface operations {
|
||||
200: {
|
||||
content: {
|
||||
"application/json":
|
||||
| components["schemas"]["CustomHistoryView"]
|
||||
| components["schemas"]["HistoryDetailed"]
|
||||
| components["schemas"]["HistorySummary"]
|
||||
| components["schemas"]["HistoryMinimal"];
|
||||
| components["schemas"]["HistorySummary"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
@@ -15806,9 +15950,9 @@ export interface operations {
|
||||
200: {
|
||||
content: {
|
||||
"application/json":
|
||||
| components["schemas"]["CustomHistoryView"]
|
||||
| components["schemas"]["HistoryDetailed"]
|
||||
| components["schemas"]["HistorySummary"]
|
||||
| components["schemas"]["HistoryMinimal"];
|
||||
| components["schemas"]["HistorySummary"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
|
||||
@@ -14,13 +14,13 @@ const props = defineProps<Props>();
|
||||
const userOverQuota = ref(false);
|
||||
|
||||
const hasMessages = computed(() => {
|
||||
return userOverQuota.value || props.history.isDeleted;
|
||||
return userOverQuota.value || props.history.deleted;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="hasMessages" class="mx-3 my-2">
|
||||
<BAlert :show="history.isDeleted" variant="warning">
|
||||
<BAlert :show="history.deleted" variant="warning">
|
||||
{{ localize("This history has been deleted") }}
|
||||
</BAlert>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
faArchive,
|
||||
faBars,
|
||||
faColumns,
|
||||
faCopy,
|
||||
faExchangeAlt,
|
||||
@@ -42,6 +43,7 @@ import SelectorModal from "@/components/History/Modals/SelectorModal.vue";
|
||||
|
||||
library.add(
|
||||
faArchive,
|
||||
faBars,
|
||||
faColumns,
|
||||
faCopy,
|
||||
faExchangeAlt,
|
||||
@@ -126,6 +128,7 @@ function userTitle(title: string) {
|
||||
|
||||
<BDropdown
|
||||
v-b-tooltip.top.hover.noninteractive
|
||||
no-caret
|
||||
size="sm"
|
||||
variant="link"
|
||||
toggle-class="text-decoration-none"
|
||||
@@ -133,6 +136,7 @@ function userTitle(title: string) {
|
||||
title="History options"
|
||||
data-description="history options">
|
||||
<template v-slot:button-content>
|
||||
<FontAwesomeIcon fixed-width :icon="faBars" />
|
||||
<span class="sr-only">History Options</span>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import axios from "axios";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import { getPermissions, isHistoryPrivate, makePrivate, type PermissionsResponse } from "@/components/History/services";
|
||||
import { prependPath } from "@/utils/redirect";
|
||||
import { errorMessageAsString } from "@/utils/simple-error";
|
||||
|
||||
@@ -48,7 +49,26 @@ const emit = defineEmits<{
|
||||
(e: "updated", id: string | null): void;
|
||||
}>();
|
||||
|
||||
async function handleSubmit(preferredObjectStoreId: string | null) {
|
||||
async function handleSubmit(preferredObjectStoreId: string | null, isPrivate: boolean) {
|
||||
if (isPrivate) {
|
||||
const { data } = await getPermissions(props.history.id);
|
||||
const permissionResponse = data as PermissionsResponse;
|
||||
const historyPrivate = await isHistoryPrivate(permissionResponse);
|
||||
if (!historyPrivate) {
|
||||
if (
|
||||
confirm(
|
||||
"Your history is set to create sharable datasets, but the target storage location is private. Change the history configuration so new datasets are private by default?"
|
||||
)
|
||||
) {
|
||||
try {
|
||||
await makePrivate(props.history.id, permissionResponse);
|
||||
} catch {
|
||||
error.value = "Failed to update default permissions for history.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const payload = { preferred_object_store_id: preferredObjectStoreId };
|
||||
const url = prependPath(`api/histories/${props.history.id}`);
|
||||
try {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import axios from "axios";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import { initRefs, updateRefs, useCallbacks } from "@/composables/datasetPermissions";
|
||||
import { withPrefix } from "@/utils/redirect";
|
||||
|
||||
import { getPermissions, getPermissionsUrl, setPermissions } from "./services";
|
||||
|
||||
import DatasetPermissionsForm from "@/components/Dataset/DatasetPermissionsForm.vue";
|
||||
|
||||
@@ -24,7 +24,7 @@ const {
|
||||
} = initRefs();
|
||||
|
||||
const inputsUrl = computed(() => {
|
||||
return `/history/permissions?id=${props.historyId}`;
|
||||
return getPermissionsUrl(props.historyId);
|
||||
});
|
||||
|
||||
const title = "Change default dataset permissions for history";
|
||||
@@ -48,11 +48,11 @@ async function change(value: unknown) {
|
||||
DATASET_MANAGE_PERMISSIONS: [managePermissionValue],
|
||||
DATASET_ACCESS: access,
|
||||
};
|
||||
axios.put(withPrefix(inputsUrl.value), formValue).then(onSuccess).catch(onError);
|
||||
setPermissions(props.historyId, formValue).then(onSuccess).catch(onError);
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const { data } = await axios.get(withPrefix(inputsUrl.value));
|
||||
const { data } = await getPermissions(props.historyId);
|
||||
updateRefs(data.inputs, managePermissionsOptions, accessPermissionsOptions, managePermissions, accessPermissions);
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, ref, watch } from "vue";
|
||||
|
||||
import type { HistorySummary } from "@/api";
|
||||
import { type HistorySummary, userOwnsHistory } from "@/api";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
import { useUserStore } from "@/stores/userStore";
|
||||
import localize from "@/utils/localization";
|
||||
@@ -42,11 +42,11 @@ const saveTitle = computed(() => {
|
||||
const saveVariant = computed(() => {
|
||||
return loading.value ? "info" : formValid.value ? "primary" : "secondary";
|
||||
});
|
||||
const userOwnsHistory = computed(() => {
|
||||
return userStore.isRegisteredUser(currentUser.value) && currentUser.value.id == props.history?.user_id;
|
||||
const isOwner = computed(() => {
|
||||
return userOwnsHistory(currentUser.value, props.history);
|
||||
});
|
||||
const newNameValid = computed(() => {
|
||||
if (userOwnsHistory.value && name.value == props.history.name) {
|
||||
if (isOwner.value && name.value == props.history.name) {
|
||||
return false;
|
||||
}
|
||||
return name.value.length > 0;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { BAlert, BButton, BButtonGroup } from "bootstrap-vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, ref, watch } from "vue";
|
||||
|
||||
import { userOwnsHistory } from "@/api";
|
||||
import { HistoryFilters } from "@/components/History/HistoryFilters";
|
||||
import { Toast } from "@/composables/toast";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
@@ -38,7 +39,7 @@ const selectedHistories = computed<PinnedHistory[]>(() => {
|
||||
} else {
|
||||
// get the latest four histories
|
||||
return [...histories.value]
|
||||
.filter((h) => !h.user_id || (!currentUser.value?.isAnonymous && h.user_id === currentUser.value?.id))
|
||||
.filter((h) => userOwnsHistory(currentUser.value, h))
|
||||
.sort((a, b) => {
|
||||
if (a.update_time < b.update_time) {
|
||||
return 1;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import axios from "axios";
|
||||
|
||||
import { fetcher } from "@/api/schema";
|
||||
import { type Input, permissionInputParts } from "@/composables/datasetPermissions";
|
||||
import type Filtering from "@/utils/filtering";
|
||||
import { withPrefix } from "@/utils/redirect";
|
||||
|
||||
const publishedHistoriesFetcher = fetcher.path("/api/histories/published").method("get").create();
|
||||
export async function getPublishedHistories(
|
||||
@@ -27,3 +31,37 @@ export async function getPublishedHistories(
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getPermissionsUrl(historyId: string) {
|
||||
return `/history/permissions?id=${historyId}`;
|
||||
}
|
||||
|
||||
export interface PermissionsResponse {
|
||||
inputs: Input[];
|
||||
}
|
||||
|
||||
export function getPermissions(historyId: string) {
|
||||
const permissionsUrl = getPermissionsUrl(historyId);
|
||||
return axios.get(withPrefix(permissionsUrl));
|
||||
}
|
||||
|
||||
export function setPermissions(historyId: string, formContents: object) {
|
||||
const permissionsUrl = getPermissionsUrl(historyId);
|
||||
return axios.put(withPrefix(permissionsUrl), formContents);
|
||||
}
|
||||
|
||||
export function makePrivate(historyId: string, permissionResponse: PermissionsResponse) {
|
||||
const { manageInput } = permissionInputParts(permissionResponse.inputs);
|
||||
const managePermissionValue: number = manageInput.value[0] as number;
|
||||
const access = [managePermissionValue];
|
||||
const formValue = {
|
||||
DATASET_MANAGE_PERMISSIONS: [managePermissionValue],
|
||||
DATASET_ACCESS: access,
|
||||
};
|
||||
return setPermissions(historyId, formValue);
|
||||
}
|
||||
|
||||
export async function isHistoryPrivate(permissionResponse: PermissionsResponse) {
|
||||
const { accessInput } = permissionInputParts(permissionResponse.inputs);
|
||||
return accessInput.value.length >= 1;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import { ConcreteObjectStoreModel } from "@/api";
|
||||
import { useObjectStoreStore } from "@/stores/objectStoreStore";
|
||||
|
||||
import ObjectStoreSelectButton from "./ObjectStoreSelectButton.vue";
|
||||
@@ -42,15 +43,17 @@ function variant(objectStoreId: string) {
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "onSubmit", id: string | null): void;
|
||||
(e: "onSubmit", id: string | null, isPrivate: boolean): void;
|
||||
}>();
|
||||
|
||||
const error = computed(() => {
|
||||
return props.parentError || loadErrorMessage.value;
|
||||
});
|
||||
|
||||
async function handleSubmit(preferredObjectStoreId: string) {
|
||||
emit("onSubmit", preferredObjectStoreId);
|
||||
async function handleSubmit(preferredObjectStore: ConcreteObjectStoreModel | null) {
|
||||
const id: string | null = (preferredObjectStore ? preferredObjectStore.object_store_id : null) as string | null;
|
||||
const isPrivate: boolean = preferredObjectStore ? preferredObjectStore.private : false;
|
||||
emit("onSubmit", id, isPrivate);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -79,7 +82,7 @@ async function handleSubmit(preferredObjectStoreId: string) {
|
||||
:object-store="objectStore"
|
||||
:variant="variant(objectStore.object_store_id)"
|
||||
class="preferred-object-store-select-button"
|
||||
@click="handleSubmit(objectStore.object_store_id)" />
|
||||
@click="handleSubmit(objectStore)" />
|
||||
</b-button-group>
|
||||
</b-col>
|
||||
<b-col cols="5">
|
||||
|
||||
@@ -45,7 +45,7 @@ const defaultExtra = () =>
|
||||
|
||||
const item = ref<Item>({
|
||||
title: "title",
|
||||
username_and_slug: "username/slug",
|
||||
username_and_slug: "__username__/__slug__",
|
||||
importable: false,
|
||||
published: false,
|
||||
users_shared_with: [],
|
||||
@@ -208,6 +208,8 @@ function onPublish(published: boolean) {
|
||||
const hasUsername = ref(Boolean(getGalaxyInstance().user.get("username")));
|
||||
const newUsername = ref("");
|
||||
|
||||
const slugSet = computed(() => itemUrl.slug != "__slug__" && itemUrl.prefix != "__username__");
|
||||
|
||||
async function setUsername() {
|
||||
axios
|
||||
.put(`${getAppRoot()}api/users/${getGalaxyInstance().user.id}/information/inputs`, {
|
||||
@@ -258,13 +260,21 @@ const embedable = computed(() => item.value.importable && props.modelClass.toLoc
|
||||
</div>
|
||||
|
||||
<div v-if="item.importable" class="mb-4">
|
||||
<div>This {{ modelClass }} is currently {{ itemStatus }}.</div>
|
||||
<p>Anyone can view and import this {{ modelClass }} by visiting the following URL:</p>
|
||||
<EditableUrl
|
||||
:prefix="itemUrl.prefix"
|
||||
:slug="itemUrl.slug"
|
||||
@change="onChangeSlug"
|
||||
@submit="onSubmitSlug" />
|
||||
<div v-if="slugSet">
|
||||
<p>
|
||||
This {{ modelClass }} is currently {{ itemStatus }}.
|
||||
<br />
|
||||
Anyone can view and import this {{ modelClass }} by visiting the following URL:
|
||||
</p>
|
||||
<EditableUrl
|
||||
:prefix="itemUrl.prefix"
|
||||
:slug="itemUrl.slug"
|
||||
@change="onChangeSlug"
|
||||
@submit="onSubmitSlug" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>Currently publishing {{ modelClass }}. A shareable URL will be available here momentarily.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="mb-4">
|
||||
Access to this {{ modelClass }} is currently restricted so that only you and the users listed below can
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
v-if="plugin.logo"
|
||||
alt="ui thumbnails"
|
||||
class="plugin-list-image"
|
||||
:src="plugin.logo" />
|
||||
:src="absPath(plugin.logo)" />
|
||||
<div v-else class="plugin-list-icon fa fa-eye" />
|
||||
</td>
|
||||
<td>
|
||||
@@ -64,6 +64,7 @@ import axios from "axios";
|
||||
import DelayedInput from "components/Common/DelayedInput";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import _l from "utils/localization";
|
||||
import { absPath } from "utils/redirect";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -106,6 +107,7 @@ export default {
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
absPath,
|
||||
onSearch(newValue) {
|
||||
this.search = newValue;
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ interface InputOption {
|
||||
roleValue: number;
|
||||
}
|
||||
|
||||
interface Input {
|
||||
export interface Input {
|
||||
value: number[];
|
||||
options: [string, number][];
|
||||
}
|
||||
@@ -46,6 +46,12 @@ export function initRefs() {
|
||||
};
|
||||
}
|
||||
|
||||
export function permissionInputParts(inputs: Input[]) {
|
||||
const manageInput: Input = inputs[0] as Input;
|
||||
const accessInput: Input = inputs[1] as Input;
|
||||
return { manageInput, accessInput };
|
||||
}
|
||||
|
||||
export function updateRefs(
|
||||
inputs: Input[],
|
||||
managePermissionsOptions: Ref<InputOption[]>,
|
||||
@@ -53,8 +59,7 @@ export function updateRefs(
|
||||
managePermissions: Ref<number[]>,
|
||||
accessPermissions: Ref<number[]>
|
||||
) {
|
||||
const manageInput: Input = inputs[0] as Input;
|
||||
const accessInput: Input = inputs[1] as Input;
|
||||
const { manageInput, accessInput } = permissionInputParts(inputs);
|
||||
managePermissionsOptions.value = manageInput.options.map((v: [string, number]) => {
|
||||
return <InputOption>{ roleName: v[0], roleValue: v[1] };
|
||||
});
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useLocalStorage } from "@vueuse/core";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, type Ref, ref, watch } from "vue";
|
||||
|
||||
import { GenericUser, useUserStore } from "@/stores/userStore";
|
||||
import type { GenericUser } from "@/api";
|
||||
import { useUserStore } from "@/stores/userStore";
|
||||
|
||||
async function hash32(value: string): Promise<string> {
|
||||
const valueUtf8 = new TextEncoder().encode(value);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import { computed, customRef, type Ref, ref } from "vue";
|
||||
|
||||
import type { GenericUser } from "@/stores/userStore";
|
||||
import type { GenericUser } from "@/api";
|
||||
|
||||
import { useHashedUserId } from "./hashedUserId";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineStore } from "pinia";
|
||||
import Vue, { computed, ref } from "vue";
|
||||
import { computed, del, ref, set } from "vue";
|
||||
|
||||
import type { HistorySummary } from "@/api";
|
||||
import { archiveHistory, unarchiveHistory } from "@/api/histories.archived";
|
||||
@@ -96,11 +96,16 @@ export const useHistoryStore = defineStore("historyStore", () => {
|
||||
}
|
||||
|
||||
function setFilterText(historyId: string, filterText: string) {
|
||||
Vue.set(storedFilterTexts.value, historyId, filterText);
|
||||
set(storedFilterTexts.value, historyId, filterText);
|
||||
}
|
||||
|
||||
function setHistory(history: HistorySummary) {
|
||||
Vue.set(storedHistories.value, history.id, history);
|
||||
if (storedHistories.value[history.id] !== undefined) {
|
||||
// Merge the incoming history with existing one to keep additional information
|
||||
Object.assign(storedHistories.value[history.id]!, history);
|
||||
} else {
|
||||
set(storedHistories.value, history.id, history);
|
||||
}
|
||||
}
|
||||
|
||||
function setHistories(histories: HistorySummary[]) {
|
||||
@@ -179,7 +184,7 @@ export const useHistoryStore = defineStore("historyStore", () => {
|
||||
} else {
|
||||
await createNewHistory();
|
||||
}
|
||||
Vue.delete(storedHistories.value, deletedHistory.id);
|
||||
del(storedHistories.value, deletedHistory.id);
|
||||
unpinHistories([deletedHistory.id]);
|
||||
await handleTotalCountChange(1, true);
|
||||
}
|
||||
@@ -241,12 +246,14 @@ export const useHistoryStore = defineStore("historyStore", () => {
|
||||
async function loadHistoryById(historyId: string) {
|
||||
if (!isLoadingHistory.has(historyId)) {
|
||||
isLoadingHistory.add(historyId);
|
||||
await getHistoryByIdFromServer(historyId)
|
||||
.then((history) => setHistory(history as HistorySummary))
|
||||
.catch((error: Error) => console.warn(error))
|
||||
.finally(() => {
|
||||
isLoadingHistory.delete(historyId);
|
||||
});
|
||||
try {
|
||||
const history = await getHistoryByIdFromServer(historyId);
|
||||
setHistory(history as HistorySummary);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
isLoadingHistory.delete(historyId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import type { components } from "@/api/schema";
|
||||
import type { AnonymousUser, User } from "@/api";
|
||||
import { useUserLocalStorage } from "@/composables/userLocalStorage";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
import {
|
||||
@@ -11,25 +11,6 @@ import {
|
||||
setCurrentThemeQuery,
|
||||
} from "@/stores/users/queries";
|
||||
|
||||
type QuotaUsageResponse = components["schemas"]["UserQuotaUsage"];
|
||||
|
||||
export interface User extends QuotaUsageResponse {
|
||||
id: string;
|
||||
email: string;
|
||||
tags_used: string[];
|
||||
isAnonymous: false;
|
||||
is_admin?: boolean;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface AnonymousUser {
|
||||
isAnonymous: true;
|
||||
username?: string;
|
||||
is_admin?: false;
|
||||
}
|
||||
|
||||
export type GenericUser = User | AnonymousUser;
|
||||
|
||||
interface Preferences {
|
||||
theme: string;
|
||||
favorites: { tools: string[] };
|
||||
@@ -138,10 +119,6 @@ export const useUserStore = defineStore("userStore", () => {
|
||||
toggledSideBar.value = toggledSideBar.value === currentOpen ? "" : currentOpen;
|
||||
}
|
||||
|
||||
function isRegisteredUser(user: User | AnonymousUser | null): user is User {
|
||||
return !user?.isAnonymous;
|
||||
}
|
||||
|
||||
return {
|
||||
currentUser,
|
||||
currentPreferences,
|
||||
@@ -157,7 +134,6 @@ export const useUserStore = defineStore("userStore", () => {
|
||||
addFavoriteTool,
|
||||
removeFavoriteTool,
|
||||
toggleSideBar,
|
||||
isRegisteredUser,
|
||||
$reset,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -44,8 +44,6 @@ export const useUserTagsStore = defineStore("userTagsStore", () => {
|
||||
if (userHash) {
|
||||
tags.value = await db.tags.where("userHash").equals(userHash).sortBy("lastUsed");
|
||||
|
||||
console.log(tags.value);
|
||||
|
||||
if (tags.value.length > maxDbEntriesPerUser) {
|
||||
await removeOldestEntries(tags.value.length - maxDbEntriesPerUser);
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ from galaxy.model import (
|
||||
Task,
|
||||
)
|
||||
from galaxy.model.base import transaction
|
||||
from galaxy.model.store import copy_dataset_instance_metadata_attributes
|
||||
from galaxy.model.store.discover import MaxDiscoveredFilesExceededError
|
||||
from galaxy.objectstore import ObjectStorePopulator
|
||||
from galaxy.structured_app import MinimalManagerApp
|
||||
@@ -1948,9 +1949,7 @@ class MinimalJobWrapper(HasResourceParameters):
|
||||
]
|
||||
|
||||
for dataset_assoc in output_dataset_associations:
|
||||
if getattr(dataset_assoc.dataset, "discovered", False):
|
||||
# skip outputs that have been discovered
|
||||
continue
|
||||
is_discovered_dataset = getattr(dataset_assoc.dataset, "discovered", False)
|
||||
context = self.get_dataset_finish_context(job_context, dataset_assoc)
|
||||
# should this also be checking library associations? - can a library item be added from a history before the job has ended? -
|
||||
# lets not allow this to occur
|
||||
@@ -1959,6 +1958,12 @@ class MinimalJobWrapper(HasResourceParameters):
|
||||
dataset_assoc.dataset.dataset.history_associations
|
||||
+ dataset_assoc.dataset.dataset.library_associations
|
||||
):
|
||||
if is_discovered_dataset:
|
||||
if dataset is dataset_assoc.dataset:
|
||||
continue
|
||||
elif dataset.extension == dataset_assoc.dataset.extension or dataset.extension == "auto":
|
||||
copy_dataset_instance_metadata_attributes(dataset_assoc.dataset, dataset)
|
||||
continue
|
||||
output_name = dataset_assoc.name
|
||||
|
||||
# Handles retry internally on error for instance...
|
||||
|
||||
@@ -977,8 +977,8 @@ class PulsarJobRunner(AsynchronousJobRunner):
|
||||
remote_job_id = full_status["job_id"]
|
||||
if len(remote_job_id) == 32:
|
||||
# It is a UUID - assign_ids = uuid in destination params...
|
||||
stmt = select(model.Job).filter(model.Job.job_runner_external_id == remote_job_id)
|
||||
galaxy_job_id = self.app.model.session.execute(stmt).scalar_one().id
|
||||
stmt = select(model.Job.id).filter(model.Job.job_runner_external_id == remote_job_id)
|
||||
galaxy_job_id = self.app.model.session.execute(stmt).scalar_one()
|
||||
else:
|
||||
galaxy_job_id = remote_job_id
|
||||
job, job_wrapper = self.app.job_manager.job_handler.job_queue.job_pair_for_id(galaxy_job_id)
|
||||
|
||||
@@ -183,9 +183,9 @@ class HistoryContentsManager(base.SortableManager):
|
||||
hdca_select = self._active_counts_statement(model.HistoryDatasetCollectionAssociation, history.id)
|
||||
subquery = hda_select.union_all(hdca_select).subquery()
|
||||
statement = select(
|
||||
cast(func.sum(subquery.c.deleted), Integer).label("deleted"),
|
||||
cast(func.sum(subquery.c.hidden), Integer).label("hidden"),
|
||||
cast(func.sum(subquery.c.active), Integer).label("active"),
|
||||
cast(func.coalesce(func.sum(subquery.c.deleted), 0), Integer).label("deleted"),
|
||||
cast(func.coalesce(func.sum(subquery.c.hidden), 0), Integer).label("hidden"),
|
||||
cast(func.coalesce(func.sum(subquery.c.active), 0), Integer).label("active"),
|
||||
)
|
||||
returned = self.app.model.context.execute(statement).one()
|
||||
return dict(returned._mapping)
|
||||
|
||||
@@ -11137,23 +11137,27 @@ mapper_registry.map_imperatively(
|
||||
# ----------------------------------------------------------------------------------------
|
||||
# The following statements must not precede the mapped models defined above.
|
||||
|
||||
Job.any_output_dataset_collection_instances_deleted = column_property(
|
||||
exists(HistoryDatasetCollectionAssociation.id).where(
|
||||
and_(
|
||||
Job.id == JobToOutputDatasetCollectionAssociation.job_id,
|
||||
HistoryDatasetCollectionAssociation.id == JobToOutputDatasetCollectionAssociation.dataset_collection_id,
|
||||
HistoryDatasetCollectionAssociation.deleted == true(),
|
||||
)
|
||||
Job.any_output_dataset_collection_instances_deleted = deferred(
|
||||
column_property(
|
||||
exists(HistoryDatasetCollectionAssociation.id).where(
|
||||
and_(
|
||||
Job.id == JobToOutputDatasetCollectionAssociation.job_id,
|
||||
HistoryDatasetCollectionAssociation.id == JobToOutputDatasetCollectionAssociation.dataset_collection_id,
|
||||
HistoryDatasetCollectionAssociation.deleted == true(),
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
Job.any_output_dataset_deleted = column_property(
|
||||
exists(HistoryDatasetAssociation.id).where(
|
||||
and_(
|
||||
Job.id == JobToOutputDatasetAssociation.job_id,
|
||||
HistoryDatasetAssociation.table.c.id == JobToOutputDatasetAssociation.dataset_id,
|
||||
HistoryDatasetAssociation.table.c.deleted == true(),
|
||||
)
|
||||
Job.any_output_dataset_deleted = deferred(
|
||||
column_property(
|
||||
exists(HistoryDatasetAssociation.id).where(
|
||||
and_(
|
||||
Job.id == JobToOutputDatasetAssociation.job_id,
|
||||
HistoryDatasetAssociation.table.c.id == JobToOutputDatasetAssociation.dataset_id,
|
||||
HistoryDatasetAssociation.table.c.deleted == true(),
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -481,12 +481,11 @@ class ModelImportStore(metaclass=abc.ABCMeta):
|
||||
if (
|
||||
dataset_association is not dataset_instance
|
||||
and dataset_association.extension == dataset_instance.extension
|
||||
or dataset_association.extension == "auto"
|
||||
):
|
||||
dataset_association.metadata = dataset_instance.metadata
|
||||
dataset_association.blurb = dataset_instance.blurb
|
||||
dataset_association.peek = dataset_instance.peek
|
||||
dataset_association.info = dataset_instance.info
|
||||
dataset_association.tool_version = dataset_instance.tool_version
|
||||
copy_dataset_instance_metadata_attributes(
|
||||
source=dataset_instance, target=dataset_association
|
||||
)
|
||||
if job:
|
||||
dataset_instance.dataset.job_id = job.id
|
||||
|
||||
@@ -3085,3 +3084,12 @@ def payload_to_source_uri(payload) -> str:
|
||||
dump(store_dict, f)
|
||||
source_uri = f"file://{import_json}"
|
||||
return source_uri
|
||||
|
||||
|
||||
def copy_dataset_instance_metadata_attributes(source: model.DatasetInstance, target: model.DatasetInstance) -> None:
|
||||
target.metadata = source.metadata
|
||||
target.blurb = source.blurb
|
||||
target.peek = source.peek
|
||||
target.info = source.info
|
||||
target.tool_version = source.tool_version
|
||||
target.extension = source.extension
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import typing
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
create_model,
|
||||
Field,
|
||||
)
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
|
||||
class BootstrapAdminUser(BaseModel):
|
||||
@@ -110,3 +119,45 @@ class PdfDocumentType(str, Enum):
|
||||
class APIKeyModel(BaseModel):
|
||||
key: str = Field(..., title="Key", description="API key to interact with the Galaxy API")
|
||||
create_time: datetime = Field(..., title="Create Time", description="The time and date this API key was created.")
|
||||
|
||||
|
||||
T = TypeVar("T", bound="BaseModel")
|
||||
|
||||
|
||||
# TODO: This is a workaround to make all fields optional.
|
||||
# It should be removed when Python/pydantic supports this feature natively.
|
||||
# https://github.com/pydantic/pydantic/issues/1673
|
||||
def partial_model(
|
||||
include: Optional[List[str]] = None, exclude: Optional[List[str]] = None
|
||||
) -> Callable[[Type[T]], Type[T]]:
|
||||
"""Decorator to make all model fields optional"""
|
||||
|
||||
if exclude is None:
|
||||
exclude = []
|
||||
|
||||
@typing.no_type_check # Mypy doesn't understand pydantic's create_model
|
||||
def decorator(model: Type[T]) -> Type[T]:
|
||||
def make_optional(field: FieldInfo, default: Any = None) -> Tuple[Any, FieldInfo]:
|
||||
new = deepcopy(field)
|
||||
new.default = default
|
||||
new.annotation = Optional[field.annotation or Any]
|
||||
return new.annotation, new
|
||||
|
||||
fields = model.model_fields
|
||||
if include is None:
|
||||
fields = fields.items()
|
||||
else:
|
||||
fields = ((k, v) for k, v in fields.items() if k in include)
|
||||
|
||||
return create_model(
|
||||
model.__name__,
|
||||
__base__=model,
|
||||
__module__=model.__module__,
|
||||
**{
|
||||
field_name: make_optional(field_info)
|
||||
for field_name, field_info in fields
|
||||
if exclude is None or field_name not in exclude
|
||||
},
|
||||
)
|
||||
|
||||
return decorator
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import (
|
||||
get_origin,
|
||||
TYPE_CHECKING,
|
||||
Union,
|
||||
)
|
||||
|
||||
from pydantic import (
|
||||
BeforeValidator,
|
||||
@@ -112,6 +116,11 @@ def literal_to_value(arg):
|
||||
return val[0]
|
||||
|
||||
|
||||
def is_optional(field):
|
||||
args = get_args(field)
|
||||
return get_origin(field) is Union and len(args) == 2 and type(None) in args
|
||||
|
||||
|
||||
def ModelClassField(default_value):
|
||||
"""Represents a database model class name annotated as a constant
|
||||
pydantic Field.
|
||||
|
||||
+51
-28
@@ -33,11 +33,13 @@ from typing_extensions import (
|
||||
Literal,
|
||||
)
|
||||
|
||||
from galaxy.schema import partial_model
|
||||
from galaxy.schema.bco import XrefItem
|
||||
from galaxy.schema.fields import (
|
||||
DecodedDatabaseIdField,
|
||||
EncodedDatabaseIdField,
|
||||
EncodedLibraryFolderDatabaseIdField,
|
||||
is_optional,
|
||||
LibraryFolderDatabaseIdField,
|
||||
literal_to_value,
|
||||
ModelClassField,
|
||||
@@ -312,8 +314,11 @@ class WithModelClass:
|
||||
def set_default(cls, data):
|
||||
if isinstance(data, dict):
|
||||
if "model_class" not in data and issubclass(cls, BaseModel):
|
||||
model_class_annotation = cls.model_fields["model_class"].annotation
|
||||
if is_optional(model_class_annotation):
|
||||
return data
|
||||
data = data.copy()
|
||||
data["model_class"] = literal_to_value(cls.model_fields["model_class"].annotation)
|
||||
data["model_class"] = literal_to_value(model_class_annotation)
|
||||
return data
|
||||
|
||||
|
||||
@@ -1064,12 +1069,6 @@ class HDCADetailed(HDCASummary):
|
||||
)
|
||||
|
||||
|
||||
class HistoryBase(Model):
|
||||
"""Provides basic configuration for all the History models."""
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
class HistoryContentItemBase(Model):
|
||||
"""Identifies a dataset or collection contained in a History."""
|
||||
|
||||
@@ -1094,7 +1093,7 @@ class UpdateContentItem(HistoryContentItem):
|
||||
model_config = ConfigDict(use_enum_values=True, extra="allow")
|
||||
|
||||
|
||||
class UpdateHistoryContentsBatchPayload(HistoryBase):
|
||||
class UpdateHistoryContentsBatchPayload(Model):
|
||||
"""Contains property values that will be updated for all the history `items` provided."""
|
||||
|
||||
items: List[UpdateContentItem] = Field(
|
||||
@@ -1103,12 +1102,13 @@ class UpdateHistoryContentsBatchPayload(HistoryBase):
|
||||
description="A list of content items to update with the changes.",
|
||||
)
|
||||
model_config = ConfigDict(
|
||||
extra="allow",
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"items": [{"history_content_type": "dataset", "id": "string"}],
|
||||
"visible": False,
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1166,7 +1166,7 @@ class HistoryContentBulkOperationResult(Model):
|
||||
errors: List[BulkOperationItemError]
|
||||
|
||||
|
||||
class UpdateHistoryContentsPayload(HistoryBase):
|
||||
class UpdateHistoryContentsPayload(Model):
|
||||
"""Can contain arbitrary/dynamic fields that will be updated for a particular history item."""
|
||||
|
||||
name: Optional[str] = Field(
|
||||
@@ -1195,28 +1195,17 @@ class UpdateHistoryContentsPayload(HistoryBase):
|
||||
description="A list of tags to add to this item.",
|
||||
)
|
||||
model_config = ConfigDict(
|
||||
extra="allow",
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"visible": False,
|
||||
"annotation": "Test",
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class HistoryMinimal(HistoryBase, WithModelClass):
|
||||
"""Minimal History Response with optional fields"""
|
||||
|
||||
model_class: HISTORY_MODEL_CLASS = ModelClassField(HISTORY_MODEL_CLASS)
|
||||
id: Optional[HistoryID] = None
|
||||
user_id: Optional[EncodedDatabaseIdField] = Field(
|
||||
None,
|
||||
title="User ID",
|
||||
description="The encoded ID of the user that owns this History.",
|
||||
)
|
||||
|
||||
|
||||
class HistorySummary(HistoryBase, WithModelClass):
|
||||
class HistorySummary(Model, WithModelClass):
|
||||
"""History summary information."""
|
||||
|
||||
model_class: HISTORY_MODEL_CLASS = ModelClassField(HISTORY_MODEL_CLASS)
|
||||
@@ -1278,9 +1267,13 @@ class HistoryActiveContentCounts(Model):
|
||||
)
|
||||
|
||||
|
||||
# TODO: https://github.com/galaxyproject/galaxy/issues/17785
|
||||
HistoryStateCounts = Dict[DatasetState, int]
|
||||
HistoryStateIds = Dict[DatasetState, List[DecodedDatabaseIdField]]
|
||||
|
||||
HistoryContentStates = Union[DatasetState, DatasetCollectionPopulatedState]
|
||||
HistoryContentStateCounts = Dict[HistoryContentStates, int]
|
||||
|
||||
|
||||
class HistoryDetailed(HistorySummary): # Equivalent to 'dev-detailed' view, which seems the default
|
||||
"""History detailed information."""
|
||||
@@ -1341,10 +1334,40 @@ class HistoryDetailed(HistorySummary): # Equivalent to 'dev-detailed' view, whi
|
||||
)
|
||||
|
||||
|
||||
AnyHistoryView = Union[
|
||||
HistoryDetailed,
|
||||
HistorySummary,
|
||||
HistoryMinimal,
|
||||
@partial_model()
|
||||
class CustomHistoryView(HistoryDetailed):
|
||||
"""History Response with all optional fields.
|
||||
|
||||
It is used for serializing only specific attributes using the "keys"
|
||||
query parameter. Unfortunately, we cannot know the exact fields that
|
||||
will be requested, so we have to allow all fields to be optional.
|
||||
"""
|
||||
|
||||
# Define a few more useful fields to be optional that are not part of HistoryDetailed
|
||||
contents_active: Optional[HistoryActiveContentCounts] = Field(
|
||||
default=None,
|
||||
title="Contents Active",
|
||||
description=("Contains the number of active, deleted or hidden items in a History."),
|
||||
)
|
||||
contents_states: Optional[HistoryContentStateCounts] = Field(
|
||||
default=None,
|
||||
title="Contents States",
|
||||
description="A dictionary keyed to possible dataset states and valued with the number of datasets in this history that have those states.",
|
||||
)
|
||||
nice_size: Optional[str] = Field(
|
||||
default=None,
|
||||
title="Nice Size",
|
||||
description="The total size of the contents of this history in a human-readable format.",
|
||||
)
|
||||
|
||||
|
||||
AnyHistoryView = Annotated[
|
||||
Union[
|
||||
CustomHistoryView,
|
||||
HistoryDetailed,
|
||||
HistorySummary,
|
||||
],
|
||||
Field(union_mode="left_to_right"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ def clone_repository(repository_clone_url: str, repository_file_dir: str, ctx_re
|
||||
Clone the repository up to the specified changeset_revision. No subsequent revisions will be
|
||||
present in the cloned repository.
|
||||
"""
|
||||
cmd = ["hg", "clone"]
|
||||
cmd = ["hg", "clone", "--stream"]
|
||||
if ctx_rev:
|
||||
cmd.extend(["-r", str(ctx_rev)])
|
||||
cmd.extend([repository_clone_url, repository_file_dir])
|
||||
|
||||
@@ -2875,7 +2875,7 @@ class ExpressionTool(Tool):
|
||||
break
|
||||
if copy_object is None:
|
||||
raise exceptions.MessageException("Failed to find dataset output.")
|
||||
out_data[key].copy_from(copy_object)
|
||||
out_data[key].copy_from(copy_object, include_metadata=True)
|
||||
|
||||
def parse_environment_variables(self, tool_source):
|
||||
"""Setup environment variable for inputs file."""
|
||||
|
||||
@@ -927,34 +927,28 @@ class GalaxyWebTransaction(base.DefaultWebTransaction, context.ProvidesHistoryCo
|
||||
Gets or creates a default history and associates it with the current
|
||||
session.
|
||||
"""
|
||||
|
||||
# Just return the current history if one exists and is not deleted.
|
||||
history = self.galaxy_session.current_history
|
||||
if history and not history.deleted:
|
||||
return history
|
||||
|
||||
# Look for an existing history that has the default name, is not
|
||||
# deleted, and is empty. If this exists, we associate it with the
|
||||
# current session and return it.
|
||||
user = self.galaxy_session.user
|
||||
if user:
|
||||
# Look for default history that (a) has default name + is not deleted and
|
||||
# (b) has no datasets. If suitable history found, use it; otherwise, create
|
||||
# new history.
|
||||
stmt = select(self.app.model.History).filter_by(
|
||||
user=user, name=self.app.model.History.default_name, deleted=False
|
||||
)
|
||||
unnamed_histories = self.sa_session.scalars(stmt)
|
||||
default_history = None
|
||||
for history in unnamed_histories:
|
||||
if history.empty:
|
||||
# Found suitable default history.
|
||||
default_history = history
|
||||
break
|
||||
self.set_history(history)
|
||||
return history
|
||||
|
||||
# Set or create history.
|
||||
if default_history:
|
||||
history = default_history
|
||||
self.set_history(history)
|
||||
else:
|
||||
history = self.new_history()
|
||||
|
||||
return history
|
||||
# No suitable history found, create a new one.
|
||||
return self.new_history()
|
||||
|
||||
def get_most_recent_history(self):
|
||||
"""
|
||||
|
||||
@@ -163,6 +163,7 @@ class FastAPIHistories:
|
||||
@router.get(
|
||||
"/api/histories",
|
||||
summary="Returns histories available to the current user.",
|
||||
response_model_exclude_unset=True,
|
||||
)
|
||||
def index(
|
||||
self,
|
||||
@@ -220,6 +221,7 @@ class FastAPIHistories:
|
||||
@router.get(
|
||||
"/api/histories/deleted",
|
||||
summary="Returns deleted histories for the current user.",
|
||||
response_model_exclude_unset=True,
|
||||
)
|
||||
def index_deleted(
|
||||
self,
|
||||
@@ -235,6 +237,7 @@ class FastAPIHistories:
|
||||
@router.get(
|
||||
"/api/histories/published",
|
||||
summary="Return all histories that are published.",
|
||||
response_model_exclude_unset=True,
|
||||
)
|
||||
def published(
|
||||
self,
|
||||
@@ -247,6 +250,7 @@ class FastAPIHistories:
|
||||
@router.get(
|
||||
"/api/histories/shared_with_me",
|
||||
summary="Return all histories that are shared with the current user.",
|
||||
response_model_exclude_unset=True,
|
||||
)
|
||||
def shared_with_me(
|
||||
self,
|
||||
@@ -280,6 +284,7 @@ class FastAPIHistories:
|
||||
@router.get(
|
||||
"/api/histories/most_recently_used",
|
||||
summary="Returns the most recently used history of the user.",
|
||||
response_model_exclude_unset=True,
|
||||
)
|
||||
def show_recent(
|
||||
self,
|
||||
@@ -292,6 +297,7 @@ class FastAPIHistories:
|
||||
"/api/histories/{history_id}",
|
||||
name="history",
|
||||
summary="Returns the history with the given ID.",
|
||||
response_model_exclude_unset=True,
|
||||
)
|
||||
def show(
|
||||
self,
|
||||
@@ -347,6 +353,7 @@ class FastAPIHistories:
|
||||
@router.post(
|
||||
"/api/histories",
|
||||
summary="Creates a new history.",
|
||||
response_model_exclude_unset=True,
|
||||
)
|
||||
def create(
|
||||
self,
|
||||
@@ -369,6 +376,7 @@ class FastAPIHistories:
|
||||
@router.delete(
|
||||
"/api/histories/{history_id}",
|
||||
summary="Marks the history with the given ID as deleted.",
|
||||
response_model_exclude_unset=True,
|
||||
)
|
||||
def delete(
|
||||
self,
|
||||
@@ -385,6 +393,7 @@ class FastAPIHistories:
|
||||
@router.put(
|
||||
"/api/histories/batch/delete",
|
||||
summary="Marks several histories with the given IDs as deleted.",
|
||||
response_model_exclude_unset=True,
|
||||
)
|
||||
def batch_delete(
|
||||
self,
|
||||
@@ -404,6 +413,7 @@ class FastAPIHistories:
|
||||
@router.post(
|
||||
"/api/histories/deleted/{history_id}/undelete",
|
||||
summary="Restores a deleted history with the given ID (that hasn't been purged).",
|
||||
response_model_exclude_unset=True,
|
||||
)
|
||||
def undelete(
|
||||
self,
|
||||
@@ -416,6 +426,7 @@ class FastAPIHistories:
|
||||
@router.put(
|
||||
"/api/histories/batch/undelete",
|
||||
summary="Marks several histories with the given IDs as undeleted.",
|
||||
response_model_exclude_unset=True,
|
||||
)
|
||||
def batch_undelete(
|
||||
self,
|
||||
@@ -432,6 +443,7 @@ class FastAPIHistories:
|
||||
@router.put(
|
||||
"/api/histories/{history_id}",
|
||||
summary="Updates the values for the history with the given ID.",
|
||||
response_model_exclude_unset=True,
|
||||
)
|
||||
def update(
|
||||
self,
|
||||
@@ -448,6 +460,7 @@ class FastAPIHistories:
|
||||
@router.post(
|
||||
"/api/histories/from_store",
|
||||
summary="Create histories from a model store.",
|
||||
response_model_exclude_unset=True,
|
||||
)
|
||||
def create_from_store(
|
||||
self,
|
||||
@@ -619,6 +632,7 @@ class FastAPIHistories:
|
||||
@router.put(
|
||||
"/api/histories/{history_id}/archive/restore",
|
||||
summary="Restore an archived history.",
|
||||
response_model_exclude_unset=True,
|
||||
)
|
||||
def restore_archived_history(
|
||||
self,
|
||||
|
||||
@@ -666,7 +666,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto
|
||||
hda_ids.append(item.id)
|
||||
else:
|
||||
hdca_ids.append(item.id)
|
||||
payload_dict = payload.dict(exclude_unset=True)
|
||||
payload_dict = payload.model_dump(exclude_unset=True)
|
||||
hdas = self.__datasets_for_update(trans, history, hda_ids, payload_dict)
|
||||
rval = []
|
||||
for hda in hdas:
|
||||
|
||||
@@ -106,6 +106,26 @@ class TestHistoriesApi(ApiTestCase, BaseHistories):
|
||||
assert show_response["url"] == f"/api/histories/{history_id}"
|
||||
assert show_response["contents_url"] == f"/api/histories/{history_id}/contents"
|
||||
|
||||
def test_show_respects_view(self):
|
||||
history_id = self._create_history(f"TestHistoryForShowView_{uuid4()}")["id"]
|
||||
# By default the view is "detailed"
|
||||
show_response = self._get(f"histories/{history_id}").json()
|
||||
assert "state" in show_response
|
||||
|
||||
# Change the view to summary
|
||||
show_response = self._get(f"histories/{history_id}", {"view": "summary"}).json()
|
||||
assert "state" not in show_response
|
||||
|
||||
# Expect only specific keys
|
||||
expected_keys = ["name"]
|
||||
unexpected_keys = ["id", "deleted", "state"]
|
||||
show_response = self._get(f"histories/{history_id}", {"keys": ",".join(expected_keys)}).json()
|
||||
assert len(show_response) == len(expected_keys)
|
||||
for key in expected_keys:
|
||||
assert key in show_response
|
||||
for key in unexpected_keys:
|
||||
assert key not in show_response
|
||||
|
||||
def test_show_most_recently_used(self):
|
||||
history_id = self._create_history("TestHistoryRecent")["id"]
|
||||
show_response = self._get("histories/most_recently_used").json()
|
||||
@@ -154,15 +174,26 @@ class TestHistoriesApi(ApiTestCase, BaseHistories):
|
||||
assert "state" in history
|
||||
|
||||
# Expect only specific keys
|
||||
expected_keys = ["name"]
|
||||
unexpected_keys = ["deleted", "state"]
|
||||
expected_keys = ["nice_size", "contents_active", "contents_states"]
|
||||
unexpected_keys = ["id", "deleted", "state"]
|
||||
index_response = self._get(f"histories?keys={','.join(expected_keys)}").json()
|
||||
for history in index_response:
|
||||
assert len(history) == len(expected_keys)
|
||||
for key in expected_keys:
|
||||
assert key in history
|
||||
for key in unexpected_keys:
|
||||
assert key not in history
|
||||
|
||||
# Expect combination of view and keys
|
||||
view = "summary"
|
||||
expected_keys = ["create_time", "count"]
|
||||
data = dict(view=view, keys=",".join(expected_keys))
|
||||
index_response = self._get("histories", data=data).json()
|
||||
for history in index_response:
|
||||
for key in expected_keys:
|
||||
assert key in history
|
||||
self._assert_has_keys(history, "id", "name", "url", "update_time", "deleted", "purged", "tags")
|
||||
|
||||
def test_index_search_mode_views(self):
|
||||
# Make sure there is at least one history
|
||||
expected_name_contains = "SearchMode"
|
||||
@@ -180,16 +211,27 @@ class TestHistoriesApi(ApiTestCase, BaseHistories):
|
||||
assert "state" in history
|
||||
|
||||
# Expect only specific keys
|
||||
expected_keys = ["name"]
|
||||
unexpected_keys = ["deleted", "state"]
|
||||
expected_keys = ["nice_size", "contents_active", "contents_states"]
|
||||
unexpected_keys = ["id", "deleted", "state"]
|
||||
data = dict(search=expected_name_contains, show_published=False, keys=",".join(expected_keys))
|
||||
index_response = self._get("histories", data=data).json()
|
||||
for history in index_response:
|
||||
assert len(history) == len(expected_keys)
|
||||
for key in expected_keys:
|
||||
assert key in history
|
||||
for key in unexpected_keys:
|
||||
assert key not in history
|
||||
|
||||
# Expect combination of view and keys
|
||||
view = "summary"
|
||||
expected_keys = ["create_time", "count"]
|
||||
data = dict(search=expected_name_contains, show_published=False, view=view, keys=",".join(expected_keys))
|
||||
index_response = self._get("histories", data=data).json()
|
||||
for history in index_response:
|
||||
for key in expected_keys:
|
||||
assert key in history
|
||||
self._assert_has_keys(history, "id", "name", "url", "update_time", "deleted", "purged", "tags")
|
||||
|
||||
def test_index_case_insensitive_contains_query(self):
|
||||
# Create the histories with a different user to ensure the test
|
||||
# is not conflicted with the current user's histories.
|
||||
|
||||
@@ -2006,7 +2006,7 @@ test_data:
|
||||
# Makes sure that setting metadata on expression tool data outputs
|
||||
# doesn't break result evaluation.
|
||||
with self.dataset_populator.test_history() as history_id:
|
||||
self._run_workflow(
|
||||
summary = self._run_workflow(
|
||||
"""class: GalaxyWorkflow
|
||||
inputs:
|
||||
some_file:
|
||||
@@ -2031,6 +2031,9 @@ steps:
|
||||
- __index__: 0
|
||||
value:
|
||||
__class__: RuntimeValue
|
||||
outputs:
|
||||
pick_out:
|
||||
outputSource: pick_value/data_param
|
||||
""",
|
||||
test_data="""
|
||||
some_file:
|
||||
@@ -2040,6 +2043,14 @@ some_file:
|
||||
""",
|
||||
history_id=history_id,
|
||||
)
|
||||
invocation_details = self.workflow_populator.get_invocation(summary.invocation_id, step_details=True)
|
||||
# Make sure metadata is actually available
|
||||
pick_value_hda = invocation_details["outputs"]["pick_out"]
|
||||
dataset_details = self.dataset_populator.get_history_dataset_details(
|
||||
history_id=history_id, content_id=pick_value_hda["id"]
|
||||
)
|
||||
assert dataset_details["metadata_reference_names"]
|
||||
assert dataset_details["metadata_bam_index"]
|
||||
|
||||
def test_run_workflow_simple_conditional_step(self):
|
||||
with self.dataset_populator.test_history() as history_id:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from typing import Dict
|
||||
|
||||
import pytest
|
||||
|
||||
from ..base import common
|
||||
from ..base.twilltestcase import ShedTwillTestCase
|
||||
|
||||
@@ -559,6 +561,7 @@ class TestResetAllRepositoryMetadata(ShedTwillTestCase):
|
||||
repository=filtering_repository, repository_tuples=[emboss_tuple], filepath=dependency_xml_path
|
||||
)
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_0110_reset_metadata_on_all_repositories(self):
|
||||
"""Reset metadata on all repositories, then verify that it has not changed."""
|
||||
self.login(email=common.admin_email, username=common.admin_username)
|
||||
|
||||
@@ -827,6 +827,21 @@ def test_sessionless_import_edit_datasets():
|
||||
assert d2 is not None
|
||||
|
||||
|
||||
def test_import_job_with_output_copy():
|
||||
app, h, temp_directory, import_history = _setup_simple_export({"for_edit": True})
|
||||
hda = h.active_datasets[-1]
|
||||
# Simulate a copy being made of an output hda
|
||||
copy = hda.copy(new_name="output copy")
|
||||
# set extension to auto, should be changed to real extension when finalizing job
|
||||
copy.extension = "auto"
|
||||
app.add_and_commit(copy)
|
||||
import_model_store = store.get_import_model_store_for_directory(
|
||||
temp_directory, import_options=store.ImportOptions(allow_dataset_object_edit=True, allow_edit=True), app=app
|
||||
)
|
||||
import_model_store.perform_import()
|
||||
assert copy.extension == "txt"
|
||||
|
||||
|
||||
def test_import_datasets_with_ids_fails_if_not_editing_models():
|
||||
app, h, temp_directory, import_history = _setup_simple_export({"for_edit": True})
|
||||
u = h.user
|
||||
|
||||
Reference in New Issue
Block a user