Merge branch 'release_24.0' into dev

This commit is contained in:
mvdbeek
2024-03-20 15:50:39 +01:00
35 changed files with 632 additions and 193 deletions
+51
View File
@@ -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
View File
@@ -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;
+38
View File
@@ -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">
+18 -8
View File
@@ -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;
},
+8 -3
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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";
+17 -10
View File
@@ -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 -25
View File
@@ -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,
};
});
-2
View File
@@ -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);
}
+8 -3
View File
@@ -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...
+2 -2
View File
@@ -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)
+3 -3
View File
@@ -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)
+18 -14
View File
@@ -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(),
)
),
)
)
+13 -5
View File
@@ -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
+51
View File
@@ -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
+10 -1
View File
@@ -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
View File
@@ -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"),
]
+1 -1
View File
@@ -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])
+1 -1
View File
@@ -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."""
+9 -15
View 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:
+46 -4
View File
@@ -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.
+12 -1
View File
@@ -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)
+15
View File
@@ -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