Skip to content

Kowalski filters: support new keywords #549

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 97 additions & 2 deletions extensions/skyportal/skyportal/handlers/api/kowalski_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from baselayer.app.access import auth_or_token, permissions
from baselayer.app.env import load_env
from ..base import BaseHandler
from ...models import Filter, Stream
from ...models import Filter, Stream, Allocation, User


env, cfg = load_env()
Expand Down Expand Up @@ -202,6 +202,10 @@ def patch(self, filter_id):
update_annotations:
type: boolean
description: "update annotations for existing candidates"
auto_followup:
type: boolean | dict
description: "automatically create follow-up requests for passing and autosaved candidates"

responses:
200:
content:
Expand Down Expand Up @@ -233,6 +237,15 @@ def patch(self, filter_id):
filter_id, self.current_user, raise_if_none=True
)

# get the existing filter
response = kowalski.api(
method="get",
endpoint=f"api/filters/{filter_id}",
)
if response.get("status") == "error":
return self.error(message=response.get("message"))
existing_data = response.get("data")

patch_data = {"filter_id": int(filter_id)}

if active is not None:
Expand All @@ -244,11 +257,62 @@ def patch(self, filter_id):
if not isinstance(autosave, dict):
autosave = {"active": bool(autosave)}
else:
valid_keys = {"active", "pipeline", "comment", "ignore_group_ids"}
valid_keys = {
"active",
"pipeline",
"comment",
"ignore_group_ids",
"saver_id",
}
if not set(autosave.keys()).issubset(valid_keys):
return self.error(
f"autosave dict keys must be a subset of {valid_keys}"
)
if "saver_id" in autosave:
with self.Session() as session:
# before enforcing the group_admin | system_admin permission check,
# we check if the saver_id is different from the current filter
# if it is the same, we skip the permission check
if (
autosave["saver_id"]
!= existing_data.get("autosave", {}).get("saver_id")
and not self.current_user.is_system_admin
):
filter = session.scalar(
Filter.select(session.user_or_token).where(
Filter.id == filter_id
)
)
filter_group_users = filter.group.group_users
if not any(
[
group_user.user_id == self.associated_user_object.id
and group_user.admin
for group_user in filter_group_users
]
):
return self.error(
"You do not have permission to set the saver_id for this filter"
)

saver_id = autosave["saver_id"]
if saver_id is not None:
try:
saver_id = int(saver_id)
except ValueError:
return self.error(
f"Invalid saver_id: {saver_id}, must be an integer"
)
user = session.scalar(
User.select(session.user_or_token).where(
User.id == saver_id
)
)
if user is None:
return self.error(
f"User with id {saver_id} not found, can't use as saver_id for auto_followup"
)
autosave["saver_id"] = user.id
patch_data["autosave"] = autosave
if update_annotations is not None:
patch_data["update_annotations"] = bool(update_annotations)
Expand All @@ -268,12 +332,43 @@ def patch(self, filter_id):
"priority",
"target_group_ids",
"validity_days",
"priority_order",
"radius",
"implements_update",
}
if not set(auto_followup.keys()).issubset(valid_keys):
return self.error(
f"auto_followup dict keys must be a subset of {valid_keys}"
)
# query the allocation by id
allocation_id = auto_followup.get("allocation_id", None)
if allocation_id is None:
return self.error("auto_followup dict must contain 'allocation_id' key")
with self.Session() as session:
allocation = session.scalar(
Allocation.select(session.user_or_token).where(
Allocation.id == allocation_id
)
)
if allocation is None:
return self.error(f"Allocation {allocation_id} not found")
try:
facility_api = allocation.instrument.api_class
except Exception as e:
return self.error(
f"Could not get facility API of allocation {allocation_id}: {e}"
)

priority_order = facility_api.priorityOrder
if priority_order not in ["asc", "desc"]:
return self.error(
"priority order of allocation must be one of ['asc', 'desc']"
)

auto_followup["priority_order"] = priority_order

auto_followup["implements_update"] = facility_api.implements()["update"]

patch_data["auto_followup"] = auto_followup

response = kowalski.api(
Expand Down
113 changes: 113 additions & 0 deletions extensions/skyportal/static/js/components/FilterPlugins.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,15 @@ const FilterPlugins = ({ group }) => {
const theme = useTheme();
const darkTheme = theme.palette.mode === "dark";

const profile = useSelector((state) => state.profile);

const filter = useSelector((state) => state.filter);
const filter_v = useSelector((state) => state.filter_v);
const { fid } = useParams();
const loadedId = useSelector((state) => state.filter.id);

const { users } = useSelector((state) => state.users);

const allGroups = useSelector((state) => state.groups.all);
const userAccessibleGroups = useSelector(
(state) => state.groups.userAccessible,
Expand Down Expand Up @@ -375,6 +379,7 @@ const FilterPlugins = ({ group }) => {
useState(null);
const [selectedIgnoreGroupIds, setSelectedIgnoreGroupIds] = useState([]);
const [selectedTargetGroupIds, setSelectedTargetGroupIds] = useState([]);
const [selectedSaver, setSelectedSaver] = useState(null);

useEffect(() => {
// set the allocation_id if there's one in the filter
Expand All @@ -387,6 +392,9 @@ const FilterPlugins = ({ group }) => {
if (filter_v?.autosave?.ignore_group_ids?.length > 0) {
setSelectedIgnoreGroupIds(filter_v.autosave.ignore_group_ids);
}
if (filter_v?.autosave?.saver_id) {
setSelectedSaver(filter_v.autosave.saver_id);
}
let newPipeline = (filter_v?.fv || []).filter(
(fv) => fv.fid === filter_v.active_fid,
);
Expand All @@ -404,6 +412,12 @@ const FilterPlugins = ({ group }) => {
if (filter_v?.auto_followup?.pipeline) {
setValue("pipeline_auto_followup", filter_v.auto_followup.pipeline);
}
if (filter?.autosave?.comment) {
setAutosaveComment(filter.autosave.comment);
}
if (filter?.auto_followup?.comment) {
setAutoFollowupComment(filter.auto_followup.comment);
}
}, [filter_v]);

useEffect(() => {
Expand Down Expand Up @@ -574,6 +588,42 @@ const FilterPlugins = ({ group }) => {
dispatch(filterVersionActions.fetchFilterVersion(fid));
};

const onSubmitSaveAutosaveSaver = async (e) => {
let newAutosave = filter_v.autosave;
if (typeof filter_v.autosave === "boolean") {
newAutosave = { active: filter_v.autosave };
}
if (e.target.value === "unassigned") {
newAutosave.saver_id = null;
} else {
newAutosave.saver_id = e.target.value;
}
const result = await dispatch(
filterVersionActions.editAutosave({
filter_id: filter.id,
autosave: newAutosave,
}),
);
if (result.status === "success") {
if (e.target.value === "unassigned") {
dispatch(
showNotification(
`Unassigned autosave saver, will use Kowalski's default saver`,
),
);
setSelectedSaver(null);
} else {
dispatch(
showNotification(
`Changed autosave saver to user with id ${e.target.value}`,
),
);
setSelectedSaver(e.target.value);
}
}
dispatch(filterVersionActions.fetchFilterVersion(fid));
};

const handleNew = () => {
setOpenNew(true);
};
Expand Down Expand Up @@ -1132,6 +1182,61 @@ const FilterPlugins = ({ group }) => {
</div>
)}
</div>
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "end",
gap: "1rem",
marginTop: "1rem",
}}
>
{filter_v?.fv &&
(filter_v?.autosave?.active === true ||
filter_v?.autosave === true) &&
((group?.group_users || []).filter(
(group_user) =>
group_user?.user_id === profile?.id &&
group_user?.admin === true,
).length === 1 ||
(profile?.roles || []).includes("Super admin")) && (
<div>
<InputLabel id="saverSelectLabel">
Group user to use as the saver (optional, group admin
only)
</InputLabel>
<Select
inputProps={{ MenuProps: { disableScrollLock: true } }}
labelId="groupsSelectLabel"
value={selectedSaver}
onChange={onSubmitSaveAutosaveSaver}
name="autosaveSaverSelect"
className={classes.allocationSelect}
>
<MenuItem
value={"unassigned"}
key={"unassigned"}
className={classes.SelectItem}
>
Unassigned (use default)
</MenuItem>
{(group?.group_users || [])
.filter((group_user) => group_user.can_save === true)
.map((group_user) => (
<MenuItem
value={group_user.user_id}
key={group_user.id}
className={classes.SelectItem}
>
{(users || []).find(
(user) => user.id === group_user.user_id,
)?.username || "Loading..."}
</MenuItem>
))}
</Select>
</div>
)}
</div>
<div className={classes.divider} />
{/* AUTO FOLLOWUP */}
<div style={{ display: "flex", flexDirection: "row" }}>
Expand Down Expand Up @@ -1465,6 +1570,14 @@ FilterPlugins.propTypes = {
group: PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
name: PropTypes.string,
group_users: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number,
user_id: PropTypes.number,
roles: PropTypes.arrayOf(PropTypes.string),
admin: PropTypes.bool,
}),
),
}).isRequired,
};

Expand Down
Loading