Replace legacy admin group forms with Vue GroupForm component

Replace FormGeneric-based group create/edit views with a new GroupForm
Vue component that uses the API directly. Add search/pagination support
to roles and users API endpoints for the multiselect dropdowns. Remove
legacy controller methods (create_group, manage_users_and_roles_for_group)
that loaded all users/roles upfront causing slow page loads.

Also add auto_create_role option to group creation API, fix selenium
test selector for the new component's submit button, and enable the
admin_user_display test for Playwright.
This commit is contained in:
mvdbeek
2026-04-07 15:46:11 +02:00
parent 0b3436d4b1
commit d70a6056f9
19 changed files with 470 additions and 180 deletions
+18 -1
View File
@@ -13342,6 +13342,12 @@ export interface components {
* @description Payload schema for creating a group.
*/
GroupCreatePayload: {
/**
* auto-create role
* @description If true, create a new role with the same name as the group and associate it.
* @default false
*/
auto_create_role: boolean;
/** name of the group */
name: string;
/**
@@ -40889,7 +40895,14 @@ export interface operations {
};
index_api_roles_get: {
parameters: {
query?: never;
query?: {
/** @description Search by role name or user email (for private roles). */
search?: string | null;
/** @description The maximum number of roles to return. */
limit?: number | null;
/** @description Number of roles to skip. */
offset?: number | null;
};
header?: {
/** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
"run-as"?: string | null;
@@ -43324,6 +43337,10 @@ export interface operations {
f_name?: string | null;
/** @description Filter on username OR email */
f_any?: string | null;
/** @description Maximum number of users to return. */
limit?: number | null;
/** @description Number of users to skip. */
offset?: number | null;
};
header?: {
/** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
+244
View File
@@ -0,0 +1,244 @@
<script setup lang="ts">
import "vue-multiselect/dist/vue-multiselect.min.css";
import { faSave } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BAlert, BButton, BFormCheckbox } from "bootstrap-vue";
import { ref } from "vue";
import Multiselect from "vue-multiselect";
import { useRouter } from "vue-router/composables";
import { GalaxyApi } from "@/api";
import { errorMessageAsString } from "@/utils/simple-error";
import FormInput from "@/components/Form/Elements/FormInput.vue";
import FormCard from "@/components/Form/FormCard.vue";
import FormElementLabel from "@/components/Form/FormElementLabel.vue";
import LoadingSpan from "@/components/LoadingSpan.vue";
interface UserOption {
id: string;
email: string;
}
interface RoleOption {
id: string;
name: string;
}
const props = defineProps<{
groupId?: string;
}>();
const isEditMode = !!props.groupId;
const errorMessage = ref("");
const loading = ref(false);
const groupName = ref("");
const selectedUsers = ref<UserOption[]>([]);
const selectedRoles = ref<RoleOption[]>([]);
const userOptions = ref<UserOption[]>([]);
const roleOptions = ref<RoleOption[]>([]);
const userSearch = ref("");
const roleSearch = ref("");
const autoCreateRole = ref(false);
const router = useRouter();
async function onUserSearch(searchValue: string) {
userSearch.value = searchValue;
if (searchValue.length < 3) {
userOptions.value = [...selectedUsers.value];
return;
}
const { data, error } = await GalaxyApi().GET("/api/users", {
params: { query: { f_email: searchValue, limit: 50 } },
});
if (error) {
errorMessage.value = errorMessageAsString(error);
return;
}
const selectedIds = new Set(selectedUsers.value.map((u) => u.id));
const filtered = data.filter((u) => u.email && !selectedIds.has(u.id)).map((u) => ({ id: u.id, email: u.email! }));
userOptions.value = [...selectedUsers.value, ...filtered];
}
async function onRoleSearch(searchValue: string) {
roleSearch.value = searchValue;
if (searchValue.length < 3) {
roleOptions.value = [...selectedRoles.value];
return;
}
const { data, error } = await GalaxyApi().GET("/api/roles", {
params: { query: { search: searchValue, limit: 50 } },
});
if (error) {
errorMessage.value = errorMessageAsString(error);
return;
}
const selectedIds = new Set(selectedRoles.value.map((r) => r.id));
const filtered = data.filter((r) => !selectedIds.has(r.id)).map((r) => ({ id: r.id, name: r.name }));
roleOptions.value = [...selectedRoles.value, ...filtered];
}
async function loadGroupData() {
if (!props.groupId) {
return;
}
loading.value = true;
try {
const { data: group, error: groupError } = await GalaxyApi().GET("/api/groups/{group_id}", {
params: { path: { group_id: props.groupId } },
});
if (groupError) {
errorMessage.value = errorMessageAsString(groupError);
loading.value = false;
return;
}
groupName.value = group.name;
const { data: users, error: usersError } = await GalaxyApi().GET("/api/groups/{group_id}/users", {
params: { path: { group_id: props.groupId } },
});
if (usersError) {
errorMessage.value = errorMessageAsString(usersError);
loading.value = false;
return;
}
selectedUsers.value = users.map((u) => ({
id: u.id,
email: u.email,
}));
userOptions.value = [...selectedUsers.value];
const { data: roles, error: rolesError } = await GalaxyApi().GET("/api/groups/{group_id}/roles", {
params: { path: { group_id: props.groupId } },
});
if (rolesError) {
errorMessage.value = errorMessageAsString(rolesError);
loading.value = false;
return;
}
selectedRoles.value = roles.map((r) => ({
id: r.id,
name: r.name,
}));
roleOptions.value = [...selectedRoles.value];
} catch (e) {
errorMessage.value = errorMessageAsString(e);
}
loading.value = false;
}
async function onSubmit() {
const userIds = selectedUsers.value.map((u) => u.id);
const roleIds = selectedRoles.value.map((r) => r.id);
if (isEditMode) {
const { error } = await GalaxyApi().PUT("/api/groups/{group_id}", {
params: { path: { group_id: props.groupId! } },
body: {
user_ids: userIds,
role_ids: roleIds,
},
});
if (error) {
errorMessage.value = errorMessageAsString(error);
return;
}
} else {
if (!groupName.value) {
errorMessage.value = "Please enter a group name.";
return;
}
const { error } = await GalaxyApi().POST("/api/groups", {
body: {
name: groupName.value,
user_ids: userIds,
role_ids: roleIds,
auto_create_role: autoCreateRole.value,
},
});
if (error) {
errorMessage.value = errorMessageAsString(error);
return;
}
}
router.push("/admin/groups");
}
loadGroupData();
</script>
<template>
<div>
<LoadingSpan v-if="loading" />
<div v-else>
<BAlert v-if="errorMessage" variant="danger" show>{{ errorMessage }}</BAlert>
<FormCard :title="isEditMode ? `Group '${groupName}'` : 'Create a new Group'" icon="fa-users">
<template v-slot:body>
<FormElementLabel title="Name" :required="!isEditMode" :condition="!!groupName">
<FormInput v-if="!isEditMode" id="admin-group-name-input" v-model="groupName" />
<span v-else>{{ groupName }}</span>
</FormElementLabel>
<FormElementLabel title="Users">
<Multiselect
id="admin-group-users-select"
v-model="selectedUsers"
:options="userOptions"
:clear-on-select="true"
:multiple="true"
:internal-search="false"
:max-height="300"
label="email"
track-by="id"
placeholder="Search users by email..."
@search-change="onUserSearch">
<template slot="noResult">
<div v-if="userSearch.length < 3">Enter at least 3 characters to search</div>
<div v-else>No users found</div>
</template>
<template slot="noOptions">
<div>Enter at least 3 characters to search</div>
</template>
</Multiselect>
</FormElementLabel>
<FormElementLabel title="Roles">
<Multiselect
id="admin-group-roles-select"
v-model="selectedRoles"
:options="roleOptions"
:clear-on-select="true"
:multiple="true"
:internal-search="false"
:max-height="300"
label="name"
track-by="id"
placeholder="Search roles by name..."
@search-change="onRoleSearch">
<template slot="noResult">
<div v-if="roleSearch.length < 3">Enter at least 3 characters to search</div>
<div v-else>No roles found</div>
</template>
<template slot="noOptions">
<div>Enter at least 3 characters to search</div>
</template>
</Multiselect>
</FormElementLabel>
<FormElementLabel v-if="!isEditMode" title="Auto-create role">
<BFormCheckbox v-model="autoCreateRole">
Create a new role with the same name as this group
</BFormCheckbox>
</FormElementLabel>
</template>
</FormCard>
<BButton id="admin-group-submit" class="my-2" variant="primary" @click="onSubmit">
<FontAwesomeIcon :icon="faSave" class="mr-1" />
<span v-localize>{{ isEditMode ? "Save" : "Create" }}</span>
</BButton>
</div>
</div>
</template>
@@ -16,6 +16,7 @@ import DataTypes from "@/components/admin/DataTypes.vue";
import ToolboxDependencies from "@/components/admin/Dependencies/Landing.vue";
import DisplayApplications from "@/components/admin/DisplayApplications.vue";
import ErrorStack from "@/components/admin/ErrorStack.vue";
import GroupForm from "@/components/admin/GroupForm.vue";
import JobsList from "@/components/admin/JobsList.vue";
import BroadcastForm from "@/components/admin/Notifications/BroadcastForm.vue";
import NotificationForm from "@/components/admin/Notifications/NotificationForm.vue";
@@ -206,10 +207,9 @@ export default [
},
{
path: "form/manage_users_and_roles_for_group",
component: FormGeneric,
component: GroupForm,
props: (route) => ({
url: `/admin/manage_users_and_roles_for_group?id=${route.query.id}`,
redirect: "/admin/groups",
groupId: route.query.id,
}),
},
{
@@ -226,11 +226,7 @@ export default [
},
{
path: "form/create_group",
component: FormGeneric,
props: {
url: "/admin/create_group",
redirect: "/admin/groups",
},
component: GroupForm,
},
{
path: "form/create_quota",
+1 -1
View File
@@ -1247,7 +1247,7 @@ admin:
registration_form: 'form#registration'
groups_grid: '#groups-grid'
roles_grid: '#roles-grid'
groups_create_view: '#submit:not([aria-disabled])'
groups_create_view: '#admin-group-submit'
libraries:
+12 -1
View File
@@ -52,8 +52,19 @@ class GroupsManager:
group = model.Group(name=name)
sa_session.add(group)
role_ids = list(payload.role_ids)
if payload.auto_create_role:
existing_role = sa_session.scalars(select(model.Role).where(model.Role.name == name).limit(1)).first()
if existing_role:
raise Conflict(f"A role with name '{name}' already exists")
role = model.Role(name=name, description=f"Role for group {name}")
sa_session.add(role)
sa_session.flush()
gra = model.GroupRoleAssociation(group, role)
sa_session.add(gra)
trans.app.security_agent.set_group_user_and_role_associations(
group, user_ids=payload.user_ids, role_ids=payload.role_ids
group, user_ids=payload.user_ids, role_ids=role_ids
)
sa_session.commit()
+15 -2
View File
@@ -66,8 +66,21 @@ class RoleManager(base.ModelManager[model.Role]):
return role
def list_displayable_roles(self, trans: ProvidesUserContext) -> list[Role]:
return get_displayable_roles(trans.sa_session, trans.user, trans.user_is_admin, trans.app.security_agent)
def list_displayable_roles(
self,
trans: ProvidesUserContext,
search: str | None = None,
limit: int | None = None,
offset: int = 0,
) -> list[Role]:
return get_displayable_roles(
trans.sa_session,
trans.user,
trans.user_is_admin,
search=search,
limit=limit,
offset=offset,
)
def create_role(self, trans: ProvidesUserContext, role_definition_model: RoleDefinitionModel) -> model.Role:
name = role_definition_model.name
+39 -6
View File
@@ -1,6 +1,8 @@
from sqlalchemy import (
and_,
false,
func,
or_,
select,
)
@@ -44,13 +46,44 @@ def get_roles_by_ids(session: galaxy_scoped_session, role_ids):
return session.scalars(stmt).all()
def get_displayable_roles(session, trans_user, user_is_admin, security_agent):
roles = []
def get_displayable_roles(
session,
trans_user,
user_is_admin,
search: str | None = None,
limit: int | None = None,
offset: int = 0,
):
stmt = select(Role).where(Role.deleted == false())
for role in session.scalars(stmt):
if user_is_admin or security_agent.ok_to_display(trans_user, role):
roles.append(role)
return roles
if not user_is_admin:
if trans_user:
# Non-admin users see: all non-private/non-sharing roles,
# plus their own private role and their own sharing roles.
user_role_ids = select(UserRoleAssociation.role_id).where(UserRoleAssociation.user_id == trans_user.id)
stmt = stmt.where(
or_(
~Role.type.in_((Role.types.PRIVATE, Role.types.SHARING)),
and_(Role.type.in_((Role.types.PRIVATE, Role.types.SHARING)), Role.id.in_(user_role_ids)),
)
)
else:
# Anonymous: exclude private and sharing roles entirely
stmt = stmt.where(~Role.type.in_((Role.types.PRIVATE, Role.types.SHARING)))
if search:
# LEFT JOIN to User via UserRoleAssociation for private roles only,
# so coalesce(User.email, Role.name) gives the displayed name.
stmt = stmt.outerjoin(
UserRoleAssociation,
and_(UserRoleAssociation.role_id == Role.id, Role.type == Role.types.PRIVATE),
).outerjoin(User, UserRoleAssociation.user_id == User.id)
displayed_name = func.coalesce(User.email, Role.name)
stmt = stmt.where(displayed_name.ilike(f"%{search}%"))
stmt = stmt.order_by(Role.id)
if offset:
stmt = stmt.offset(offset)
if limit is not None:
stmt = stmt.limit(limit)
return session.scalars(stmt).all()
def get_private_role_user_emails_dict(session, role_ids: set[int] | None = None) -> dict[int, str]:
+7
View File
@@ -57,6 +57,8 @@ def get_users_for_index(
is_admin: bool = False,
expose_user_email: bool = False,
expose_user_name: bool = False,
limit: int | None = None,
offset: int = 0,
) -> Sequence[User]:
stmt = select(User)
if f_email and (is_admin or expose_user_email):
@@ -77,6 +79,11 @@ def get_users_for_index(
stmt = stmt.where(User.deleted == true())
else:
stmt = stmt.where(User.deleted == false())
stmt = stmt.order_by(User.email)
if offset:
stmt = stmt.offset(offset)
if limit is not None:
stmt = stmt.limit(limit)
return session.scalars(stmt).all()
+5
View File
@@ -69,6 +69,11 @@ class GroupCreatePayload(Model):
[],
title="role IDs",
)
auto_create_role: bool = Field(
False,
title="auto-create role",
description="If true, create a new role with the same name as the group and associate it.",
)
@partial_model()
+31 -3
View File
@@ -3,8 +3,12 @@ API operations on Role objects.
"""
import logging
from typing import Optional
from fastapi import Body
from fastapi import (
Body,
Query,
)
from galaxy.managers.context import ProvidesUserContext
from galaxy.schema.schema import (
@@ -22,6 +26,24 @@ from galaxy.webapps.galaxy.services.roles import RolesService
log = logging.getLogger(__name__)
SearchRolesQueryParam: Optional[str] = Query(
default=None,
title="Search filter",
description="Search by role name or user email (for private roles).",
)
LimitRolesQueryParam: Optional[int] = Query(
default=None,
ge=1,
title="Limit",
description="The maximum number of roles to return.",
)
OffsetRolesQueryParam: Optional[int] = Query(
default=0,
ge=0,
title="Offset",
description="Number of roles to skip.",
)
# Empty paths (e.g. /api/roles) only work if a prefix is defined right here.
# https://github.com/tiangolo/fastapi/pull/415/files
@@ -33,8 +55,14 @@ class FastAPIRoles:
service: RolesService = depends(RolesService)
@router.get("/api/roles")
def index(self, trans: ProvidesUserContext = DependsOnTrans) -> RoleListResponse:
return self.service.get_index(trans=trans)
def index(
self,
trans: ProvidesUserContext = DependsOnTrans,
search: Optional[str] = SearchRolesQueryParam,
limit: Optional[int] = LimitRolesQueryParam,
offset: Optional[int] = OffsetRolesQueryParam,
) -> RoleListResponse:
return self.service.get_index(trans=trans, search=search, limit=limit, offset=offset)
@router.get("/api/roles/{id}")
def show(self, id: RoleIDPathParam, trans: ProvidesUserContext = DependsOnTrans) -> RoleModelResponse:
+7 -1
View File
@@ -640,8 +640,14 @@ class FastAPIUsers:
f_email: Optional[str] = FilterEmailQueryParam,
f_name: Optional[str] = FilterNameQueryParam,
f_any: Optional[str] = FilterAnyQueryParam,
limit: Optional[int] = Query(
default=None, ge=1, title="Limit", description="Maximum number of users to return."
),
offset: Optional[int] = Query(default=0, ge=0, title="Offset", description="Number of users to skip."),
) -> list[MaybeLimitedUserModel]:
return self.service.get_index(trans=trans, deleted=deleted, f_email=f_email, f_name=f_name, f_any=f_any)
return self.service.get_index(
trans=trans, deleted=deleted, f_email=f_email, f_name=f_name, f_any=f_any, limit=limit, offset=offset
)
@router.get(
"/api/users/{user_id}",
@@ -17,7 +17,6 @@ from galaxy.exceptions import (
RequestParameterInvalidException,
)
from galaxy.managers.quotas import QuotaManager
from galaxy.model.db.role import get_private_role_user_emails_dict
from galaxy.model.index_filter_util import (
raw_text_column_filter,
text_column_filter,
@@ -763,153 +762,6 @@ class AdminGalaxy(controller.BaseUIController):
trans.sa_session.commit()
return {"message": f"Group '{old_name}' has been renamed to '{new_name}'."}
@web.legacy_expose_api
@web.require_admin
def manage_users_and_roles_for_group(self, trans, payload=None, **kwd):
group_id = kwd.get("id")
if not group_id:
return self.message_exception(trans, f"Invalid group id ({str(group_id)}) received")
group = get_group(trans, group_id)
if trans.request.method == "GET":
in_users = []
all_users = []
in_roles = []
all_roles = []
for user in (
trans.sa_session.query(trans.app.model.User)
.filter(trans.app.model.User.table.c.deleted == false())
.order_by(trans.app.model.User.table.c.email)
):
if user in [x.user for x in group.users]:
in_users.append(trans.security.encode_id(user.id))
all_users.append((user.email, trans.security.encode_id(user.id)))
private_role_emails = get_private_role_user_emails_dict(trans.sa_session)
for role in (
trans.sa_session.query(trans.app.model.Role)
.filter(trans.app.model.Role.deleted == false())
.order_by(trans.app.model.Role.name)
):
if role in [x.role for x in group.roles]:
in_roles.append(trans.security.encode_id(role.id))
displayed_name = private_role_emails.get(role.id, role.name)
all_roles.append((displayed_name, trans.security.encode_id(role.id)))
return {
"title": f"Group '{group.name}'",
"message": f"Group '{group.name}' is currently associated with {len(in_users)} user(s) and {len(in_roles)} role(s).",
"status": "info",
"inputs": [
build_select_input("in_roles", "Roles", all_roles, in_roles),
build_select_input("in_users", "Users", all_users, in_users),
],
}
else:
user_ids = [trans.security.decode_id(id) for id in util.listify(payload.get("in_users"))]
role_ids = [trans.security.decode_id(id) for id in util.listify(payload.get("in_roles"))]
try:
trans.app.security_agent.set_group_user_and_role_associations(
group, user_ids=user_ids, role_ids=role_ids
)
return {
"message": f"Group '{group.name}' has been updated with {len(user_ids)} associated users and {len(role_ids)} associated roles."
}
except RequestParameterInvalidException:
return self.message_exception(trans, "One or more invalid user/role id has been provided.")
@web.legacy_expose_api
@web.require_admin
def create_group(self, trans, payload=None, **kwd):
if trans.request.method == "GET":
all_users = []
all_roles = []
for user in (
trans.sa_session.query(trans.app.model.User)
.filter(trans.app.model.User.table.c.deleted == false())
.order_by(trans.app.model.User.table.c.email)
):
all_users.append((user.email, trans.security.encode_id(user.id)))
private_role_emails = get_private_role_user_emails_dict(trans.sa_session)
for role in (
trans.sa_session.query(trans.app.model.Role)
.filter(trans.app.model.Role.deleted == false())
.order_by(trans.app.model.Role.name)
):
displayed_name = private_role_emails.get(role.id, role.name)
all_roles.append((displayed_name, trans.security.encode_id(role.id)))
return {
"title": "Create Group",
"title_id": "create-group",
"inputs": [
{"name": "name", "label": "Name"},
build_select_input("in_roles", "Roles", all_roles, []),
build_select_input("in_users", "Users", all_users, []),
{
"name": "auto_create",
"label": "Create a new role of the same name for this group:",
"type": "boolean",
"optional": True,
},
],
}
else:
name = util.restore_text(payload.get("name", ""))
auto_create_checked = payload.get("auto_create")
in_users = [
trans.sa_session.query(trans.app.model.User).get(trans.security.decode_id(x))
for x in util.listify(payload.get("in_users"))
]
in_roles = [
trans.sa_session.query(trans.app.model.Role).get(trans.security.decode_id(x))
for x in util.listify(payload.get("in_roles"))
]
if not name:
return self.message_exception(trans, "Enter a valid name.")
elif trans.sa_session.query(trans.app.model.Group).filter(trans.app.model.Group.name == name).first():
return self.message_exception(
trans,
"Group names must be unique and a group with that name already exists, so choose another name.",
)
elif None in in_users or None in in_roles:
return self.message_exception(trans, "One or more invalid user/role id has been provided.")
else:
# Create the role
group = trans.app.model.Group(name=name)
trans.sa_session.add(group)
# Create the UserRoleAssociations
for user in in_users:
uga = trans.app.model.UserGroupAssociation(user, group)
trans.sa_session.add(uga)
# Create the GroupRoleAssociations
for role in in_roles:
gra = trans.app.model.GroupRoleAssociation(group, role)
trans.sa_session.add(gra)
if auto_create_checked:
# Check if role with same name already exists
if trans.sa_session.query(trans.app.model.Role).filter(trans.app.model.Role.name == name).first():
return self.message_exception(
trans,
"A role with that name already exists, so choose another name or disable role creation.",
)
# Create the role
role = trans.app.model.Role(name=name, description=f"Role for group {name}")
trans.sa_session.add(role)
# Associate the group with the role
gra = trans.model.GroupRoleAssociation(group, role)
trans.sa_session.add(gra)
num_in_roles = len(in_roles) + 1
else:
num_in_roles = len(in_roles)
trans.sa_session.commit()
message = f"Group '{group.name}' has been created with {len(in_users)} associated users and {num_in_roles} associated roles."
if auto_create_checked:
message += (
"One of the roles associated with this group is the newly created role with the same name."
)
return {"message": message}
@web.expose
@web.require_admin
def create_new_user(self, trans, **kwd):
+9 -6
View File
@@ -38,14 +38,17 @@ class RolesService(ServiceBase):
super().__init__(security)
self.role_manager = role_manager
def get_index(self, trans: ProvidesUserContext) -> RoleListResponse:
roles = self.role_manager.list_displayable_roles(trans)
def get_index(
self,
trans: ProvidesUserContext,
search: Optional[str] = None,
limit: Optional[int] = None,
offset: Optional[int] = 0,
) -> RoleListResponse:
roles = self.role_manager.list_displayable_roles(trans, search=search, limit=limit, offset=offset or 0)
role_ids = {r.id for r in roles}
private_role_emails = get_private_role_user_emails_dict(trans.sa_session, role_ids=role_ids)
data = []
for role in roles:
displayed_name = private_role_emails.get(role.id, role.name)
data.append(role_to_model(role, displayed_name))
data = [role_to_model(role, private_role_emails.get(role.id, role.name)) for role in roles]
return RoleListResponse(root=data)
def show(self, trans: ProvidesUserContext, id: DecodedDatabaseIdField) -> RoleModelResponse:
@@ -204,6 +204,8 @@ class UsersService(ServiceBase):
f_email: Optional[str],
f_name: Optional[str],
f_any: Optional[str],
limit: Optional[int] = None,
offset: Optional[int] = 0,
) -> list[MaybeLimitedUserModel]:
# never give any info to non-authenticated users
if not trans.user and not trans.user_is_bootstrap_admin:
@@ -234,6 +236,8 @@ class UsersService(ServiceBase):
trans.user_is_admin,
trans.app.config.expose_user_email,
trans.app.config.expose_user_name,
limit=limit,
offset=offset or 0,
)
rval: list[MaybeLimitedUserModel] = []
for user in users:
+25
View File
@@ -31,6 +31,31 @@ class TestGroupsApi(ApiTestCase):
response = self._post("groups", payload, admin=True, json=True)
self._assert_status_code_is(response, 400)
def test_create_with_auto_create_role(self):
name = f"auto-role-group-{self.dataset_populator.get_random_name()}"
payload = self._build_valid_group_payload(name)
payload["auto_create_role"] = True
response = self._post("groups", payload, admin=True, json=True)
self._assert_status_code_is(response, 200)
group = response.json()[0]
self._assert_valid_group(group)
# Verify role with same name was created and associated with the group
roles = self._get(f"groups/{group['id']}/roles", admin=True).json()
role_names = [r["name"] for r in roles]
assert name in role_names
def test_create_with_auto_create_role_conflict(self):
"""Auto-create role should fail if a role with the same name already exists."""
name = f"auto-role-conflict-{self.dataset_populator.get_random_name()}"
# First create a role with this name
role_payload = {"name": name, "description": "A test role.", "user_ids": [self.dataset_populator.user_id()]}
self._post("roles", role_payload, admin=True, json=True)
# Now try to create a group with auto_create_role - should conflict
payload = self._build_valid_group_payload(name)
payload["auto_create_role"] = True
response = self._post("groups", payload, admin=True, json=True)
self._assert_status_code_is(response, 409)
def test_create_duplicated_name_raises_409(self):
payload = self._build_valid_group_payload()
response = self._post("groups", payload, admin=True, json=True)
+30
View File
@@ -130,6 +130,36 @@ class TestRolesApi(ApiTestCase):
response = self._get(f"roles/{different_user_role_id}")
assert_status_code_is(response, 404)
@requires_admin
def test_list_with_pagination(self):
self._create_role()
self._create_role()
# Test limit
response = self._get("roles", data={"limit": 1}, admin=True)
assert_status_code_is(response, 200)
data = response.json()
assert len(data) == 1
# Test limit + offset returns different result
response_offset = self._get("roles", data={"limit": 1, "offset": 1}, admin=True)
assert_status_code_is(response_offset, 200)
data_offset = response_offset.json()
assert len(data_offset) == 1
assert data[0]["id"] != data_offset[0]["id"]
@requires_admin
def test_list_with_search(self):
unique = self.dataset_populator.get_random_name()
role = self._create_role(name=f"searchable-{unique}")
response = self._get("roles", data={"search": unique}, admin=True)
assert_status_code_is(response, 200)
data = response.json()
assert any(r["id"] == role["id"] for r in data)
# Search for non-existing returns empty
response = self._get("roles", data={"search": "nonexistent-xyz-99999"}, admin=True)
assert_status_code_is(response, 200)
data = response.json()
assert len(data) == 0
@requires_admin
def test_create_only_admin(self):
response = self._post("roles", json=True)
+17
View File
@@ -47,6 +47,23 @@ class TestUsersApi(ApiTestCase):
all_deleted_users = all_deleted_users_response_2.json()
assert len([u for u in all_deleted_users if u["email"] == TEST_USER_EMAIL_INDEX_DELETED]) == 1
@requires_admin
def test_index_with_pagination(self):
dataset_populator = DatasetPopulator(self.galaxy_interactor)
self._setup_user(f"pagination_test_{dataset_populator.get_random_name()}@bx.psu.edu")
self._setup_user(f"pagination_test_{dataset_populator.get_random_name()}@bx.psu.edu")
response = self._get("users", data={"limit": 1}, admin=True)
self._assert_status_code_is(response, 200)
data = response.json()
assert len(data) == 1
# With offset
response_offset = self._get("users", data={"limit": 1, "offset": 1}, admin=True)
self._assert_status_code_is(response_offset, 200)
data_offset = response_offset.json()
assert len(data_offset) == 1
# Different results
assert data[0]["id"] != data_offset[0]["id"]
def test_index_anon(self):
with self._different_user(anon=True):
all_users_response = self._get("users")
@@ -182,7 +182,6 @@ class TestAdminApp(SeleniumTestCase):
assert title_element.text == "Local Data"
self.screenshot("admin_local_data")
@selenium_only("Not yet migrated to support Playwright backend")
@selenium_test
@requires_admin
def test_admin_user_display(self):
+2 -2
View File
@@ -151,8 +151,8 @@ def test_get_displayable_roles(session, make_role, make_user_and_role):
admin_role1 = make_role(type="admin", name="admin-role-1", description="Description of admin-role1")
make_role(type="admin", description="Description of admin-role1", deleted=True)
user_is_admin, security_agent = True, None
roles = get_displayable_roles(session, user1, user_is_admin, security_agent)
user_is_admin = True
roles = get_displayable_roles(session, user1, user_is_admin)
assert len(roles) == 3
assert roles[0].id == private_role1.id
assert roles[1].id == private_role2.id