From eca0dcbfb8ae51429ff5b721f23d6e5e60ffa16e Mon Sep 17 00:00:00 2001 From: Anastasia Beglova Date: Wed, 9 Oct 2024 14:32:54 -0400 Subject: [PATCH] make search defaults settable --- app.json | 4 + frontends/api/src/clients.ts | 5 + frontends/api/src/generated/v0/api.ts | 137 ++++++++++++++++++ .../api/src/hooks/adminSearchParams/index.ts | 15 ++ frontends/api/src/test-utils/urls.ts | 6 + frontends/mit-learn/jest.config.ts | 5 - .../SearchDisplay/SearchDisplay.tsx | 83 ++++++----- .../src/pages/SearchPage/SearchPage.test.tsx | 42 ++++-- frontends/mit-learn/webpack.config.js | 30 ---- .../ol-utilities/src/types/settings.d.ts | 5 - learning_resources_search/urls.py | 10 ++ learning_resources_search/views.py | 27 ++++ main/settings.py | 3 + openapi/specs/v0.yaml | 9 ++ 14 files changed, 293 insertions(+), 88 deletions(-) create mode 100644 frontends/api/src/hooks/adminSearchParams/index.ts diff --git a/app.json b/app.json index 4fe3feccae..e02f108f58 100644 --- a/app.json +++ b/app.json @@ -91,6 +91,10 @@ "description": "Default max incompleteness penalty value for the search API and frontend", "required": false }, + "DEFAULT_SEARCH_CONTENT_FILE_SCORE_WEIGHT": { + "description": "Default score weight for content file search match", + "required": false + }, "EDX_API_ACCESS_TOKEN_URL": { "description": "URL to retrieve a MITx access token", "required": false diff --git a/frontends/api/src/clients.ts b/frontends/api/src/clients.ts index 2f4781910c..d56cd7a56f 100644 --- a/frontends/api/src/clients.ts +++ b/frontends/api/src/clients.ts @@ -20,6 +20,7 @@ import { NewsEventsApi, ProfilesApi, TestimonialsApi, + LearningResourcesSearchAdminParamsApi, } from "./generated/v0/api" import axiosInstance from "./axios" @@ -40,6 +41,9 @@ const learningResourcesSearchApi = new LearningResourcesSearchApi( axiosInstance, ) +const learningResourcesSearchAdminParamsApi = + new LearningResourcesSearchAdminParamsApi(undefined, BASE_PATH, axiosInstance) + const featuredApi = new FeaturedApi(undefined, BASE_PATH, axiosInstance) const learningpathsApi = new LearningpathsApi( @@ -102,4 +106,5 @@ export { newsEventsApi, featuredApi, testimonialsApi, + learningResourcesSearchAdminParamsApi, } diff --git a/frontends/api/src/generated/v0/api.ts b/frontends/api/src/generated/v0/api.ts index 6d2b7f1290..3db3d6ef94 100644 --- a/frontends/api/src/generated/v0/api.ts +++ b/frontends/api/src/generated/v0/api.ts @@ -4520,6 +4520,143 @@ export class CkeditorApi extends BaseAPI { } } +/** + * LearningResourcesSearchAdminParamsApi - axios parameter creator + * @export + */ +export const LearningResourcesSearchAdminParamsApiAxiosParamCreator = function ( + configuration?: Configuration, +) { + return { + /** + * Learning resource search default admin param values + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + learningResourcesSearchAdminParamsRetrieve: async ( + options: RawAxiosRequestConfig = {}, + ): Promise => { + const localVarPath = `/api/v0/learning_resources_search_admin_params/` + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL) + let baseOptions + if (configuration) { + baseOptions = configuration.baseOptions + } + + const localVarRequestOptions = { + method: "GET", + ...baseOptions, + ...options, + } + const localVarHeaderParameter = {} as any + const localVarQueryParameter = {} as any + + setSearchParams(localVarUrlObj, localVarQueryParameter) + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {} + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + } + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + } + }, + } +} + +/** + * LearningResourcesSearchAdminParamsApi - functional programming interface + * @export + */ +export const LearningResourcesSearchAdminParamsApiFp = function ( + configuration?: Configuration, +) { + const localVarAxiosParamCreator = + LearningResourcesSearchAdminParamsApiAxiosParamCreator(configuration) + return { + /** + * Learning resource search default admin param values + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async learningResourcesSearchAdminParamsRetrieve( + options?: RawAxiosRequestConfig, + ): Promise< + (axios?: AxiosInstance, basePath?: string) => AxiosPromise + > { + const localVarAxiosArgs = + await localVarAxiosParamCreator.learningResourcesSearchAdminParamsRetrieve( + options, + ) + const index = configuration?.serverIndex ?? 0 + const operationBasePath = + operationServerMap[ + "LearningResourcesSearchAdminParamsApi.learningResourcesSearchAdminParamsRetrieve" + ]?.[index]?.url + return (axios, basePath) => + createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration, + )(axios, operationBasePath || basePath) + }, + } +} + +/** + * LearningResourcesSearchAdminParamsApi - factory interface + * @export + */ +export const LearningResourcesSearchAdminParamsApiFactory = function ( + configuration?: Configuration, + basePath?: string, + axios?: AxiosInstance, +) { + const localVarFp = LearningResourcesSearchAdminParamsApiFp(configuration) + return { + /** + * Learning resource search default admin param values + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + learningResourcesSearchAdminParamsRetrieve( + options?: RawAxiosRequestConfig, + ): AxiosPromise { + return localVarFp + .learningResourcesSearchAdminParamsRetrieve(options) + .then((request) => request(axios, basePath)) + }, + } +} + +/** + * LearningResourcesSearchAdminParamsApi - object-oriented interface + * @export + * @class LearningResourcesSearchAdminParamsApi + * @extends {BaseAPI} + */ +export class LearningResourcesSearchAdminParamsApi extends BaseAPI { + /** + * Learning resource search default admin param values + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LearningResourcesSearchAdminParamsApi + */ + public learningResourcesSearchAdminParamsRetrieve( + options?: RawAxiosRequestConfig, + ) { + return LearningResourcesSearchAdminParamsApiFp(this.configuration) + .learningResourcesSearchAdminParamsRetrieve(options) + .then((request) => request(this.axios, this.basePath)) + } +} + /** * NewsEventsApi - axios parameter creator * @export diff --git a/frontends/api/src/hooks/adminSearchParams/index.ts b/frontends/api/src/hooks/adminSearchParams/index.ts new file mode 100644 index 0000000000..08a46f49cf --- /dev/null +++ b/frontends/api/src/hooks/adminSearchParams/index.ts @@ -0,0 +1,15 @@ +import { useQuery } from "@tanstack/react-query" +import { learningResourcesSearchAdminParamsApi } from "../../clients" + +const useAdminSearchParams = (enabled: boolean) => + useQuery({ + queryKey: ["adminParams"], + queryFn: async () => { + const response = + await learningResourcesSearchAdminParamsApi.learningResourcesSearchAdminParamsRetrieve() + return response.data + }, + enabled: enabled, + }) + +export { useAdminSearchParams } diff --git a/frontends/api/src/test-utils/urls.ts b/frontends/api/src/test-utils/urls.ts index f440423d7f..bedc2f275b 100644 --- a/frontends/api/src/test-utils/urls.ts +++ b/frontends/api/src/test-utils/urls.ts @@ -203,6 +203,11 @@ const search = { const userMe = { get: () => `${API_BASE_URL}/api/v0/users/me/`, } + +const adminSearchParams = { + get: () => `${API_BASE_URL}/api/v0/learning_resources_search_admin_params/`, +} + const profileMe = { get: () => `${API_BASE_URL}/api/v0/profiles/me/`, patch: () => `${API_BASE_URL}/api/v0/profiles/me/`, @@ -232,4 +237,5 @@ export { departments, newsEvents, testimonials, + adminSearchParams, } diff --git a/frontends/mit-learn/jest.config.ts b/frontends/mit-learn/jest.config.ts index 54925827a7..c767e8583f 100644 --- a/frontends/mit-learn/jest.config.ts +++ b/frontends/mit-learn/jest.config.ts @@ -19,11 +19,6 @@ const config: Config.InitialOptions = { MITOL_API_BASE_URL: "https://api.test.learn.mit.edu", PUBLIC_URL: "", SITE_NAME: "MIT Learn", - DEFAULT_SEARCH_MODE: "phrase", - DEFAULT_SEARCH_SLOP: 6, - DEFAULT_SEARCH_STALENESS_PENALTY: 2.5, - DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF: 0, - DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY: 90, }, }, } diff --git a/frontends/mit-learn/src/page-components/SearchDisplay/SearchDisplay.tsx b/frontends/mit-learn/src/page-components/SearchDisplay/SearchDisplay.tsx index e82eeb00ed..c68d7f4a19 100644 --- a/frontends/mit-learn/src/page-components/SearchDisplay/SearchDisplay.tsx +++ b/frontends/mit-learn/src/page-components/SearchDisplay/SearchDisplay.tsx @@ -30,6 +30,7 @@ import { SearchModeEnumDescriptions, } from "api" import { useLearningResourcesSearch } from "api/hooks/learningResources" +import { useAdminSearchParams } from "api/hooks/adminSearchParams" import { GridColumn, GridContainer } from "@/components/GridLayout/GridLayout" import { AvailableFacets, @@ -517,14 +518,6 @@ interface SearchDisplayProps { filterHeadingEl: React.ElementType } -const { - DEFAULT_SEARCH_MODE, - DEFAULT_SEARCH_SLOP, - DEFAULT_SEARCH_STALENESS_PENALTY, - DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF, - DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY, -} = APP_SETTINGS - const SearchDisplay: React.FC = ({ page, setPage, @@ -543,6 +536,10 @@ const SearchDisplay: React.FC = ({ }) => { const [searchParams] = useSearchParams() const [expandAdminOptions, setExpandAdminOptions] = useState(false) + + const { data: adminParams, isLoading: isAdminParamsLoading } = + useAdminSearchParams(expandAdminOptions) + const scrollHook = useRef(null) const activeTab = TABS.find( @@ -628,25 +625,6 @@ const SearchDisplay: React.FC = ({ actuallyToggleParamValue(name, rawValue, checked) } - const searchModeDropdown = ( - - setSearchParams((prev) => { - const next = new URLSearchParams(prev) - next.set("search_mode", e.target.value as string) - if (e.target.value !== "phrase") { - next.delete("slop") - } - return next - }) - } - options={searchModeDropdownOptions} - className="search-mode-dropdown" - /> - ) - const sortDropdown = ( = ({ /> ) - const AdminOptions: React.FC = ( - expandAdminOptions, - setExpandAdminOptions, + type adminParamsType = { + search_mode: string + slop: number + yearly_decay_percent: number + min_score: number + max_incompleteness_penalty: number + content_file_score_weight: number + } + + const AdminOptions = ( + expandAdminOptions: boolean, + setExpandAdminOptions: (value: boolean) => void, + adminParams: adminParamsType | void | undefined, ) => { const titleLineIcon = expandAdminOptions ? "expand_less" : "expand_more" + const searchModeDropdown = ( + + setSearchParams((prev) => { + const next = new URLSearchParams(prev) + next.set("search_mode", e.target.value as string) + if (e.target.value !== "phrase") { + next.delete("slop") + } + return next + }) + } + options={searchModeDropdownOptions} + className="search-mode-dropdown" + /> + ) + return (
= ({ {titleLineIcon} - {expandAdminOptions ? ( + {expandAdminOptions && adminParams && !isAdminParamsLoading ? (
Resource Score Staleness Penalty @@ -691,7 +698,7 @@ const SearchDisplay: React.FC = ({ currentValue={ searchParams.get("yearly_decay_percent") ? Number(searchParams.get("yearly_decay_percent")) - : DEFAULT_SEARCH_STALENESS_PENALTY + : adminParams.yearly_decay_percent } setSearchParams={setSearchParams} urlParam="yearly_decay_percent" @@ -713,7 +720,7 @@ const SearchDisplay: React.FC = ({
{(!searchParams.get("search_mode") && - DEFAULT_SEARCH_MODE === "phrase") || + adminParams.search_mode === "phrase") || searchParams.get("search_mode") === "phrase" ? (
Slop @@ -722,7 +729,7 @@ const SearchDisplay: React.FC = ({ currentValue={ searchParams.get("slop") ? Number(searchParams.get("slop")) - : DEFAULT_SEARCH_SLOP + : adminParams.slop } setSearchParams={setSearchParams} urlParam="slop" @@ -742,7 +749,7 @@ const SearchDisplay: React.FC = ({ currentValue={ searchParams.get("min_score") ? Number(searchParams.get("min_score")) - : DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF + : adminParams.min_score } setSearchParams={setSearchParams} urlParam="min_score" @@ -761,7 +768,7 @@ const SearchDisplay: React.FC = ({ currentValue={ searchParams.get("max_incompleteness_penalty") ? Number(searchParams.get("max_incompleteness_penalty")) - : DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY + : adminParams.max_incompleteness_penalty } setSearchParams={setSearchParams} urlParam="max_incompleteness_penalty" @@ -783,7 +790,7 @@ const SearchDisplay: React.FC = ({ currentValue={ searchParams.get("content_file_score_weight") ? Number(searchParams.get("content_file_score_weight")) - : 1 + : adminParams.content_file_score_weight } setSearchParams={setSearchParams} urlParam="content_file_score_weight" @@ -818,7 +825,7 @@ const SearchDisplay: React.FC = ({ facetOptions={data?.metadata.aggregations ?? {}} /> {user?.is_learning_path_editor - ? AdminOptions(expandAdminOptions, setExpandAdminOptions) + ? AdminOptions(expandAdminOptions, setExpandAdminOptions, adminParams) : null} diff --git a/frontends/mit-learn/src/pages/SearchPage/SearchPage.test.tsx b/frontends/mit-learn/src/pages/SearchPage/SearchPage.test.tsx index 6370ad254a..1cb8efb1c7 100644 --- a/frontends/mit-learn/src/pages/SearchPage/SearchPage.test.tsx +++ b/frontends/mit-learn/src/pages/SearchPage/SearchPage.test.tsx @@ -248,6 +248,12 @@ describe("SearchPage", () => { const adminOptions = screen.queryByText("Admin Options") expect(adminOptions).toBeNull() }) + + expect(makeRequest).not.toHaveBeenCalledWith( + "get", + urls.adminSearchParams, + expect.anything(), + ) }) test("non admin users do not see admin options", async () => { @@ -274,12 +280,19 @@ describe("SearchPage", () => { is_authenticated: true, is_learning_path_editor: false, }) + renderWithProviders() await waitFor(() => { const adminOptions = screen.queryByText("Admin Options") expect(adminOptions).toBeNull() }) + + expect(makeRequest).not.toHaveBeenCalledWith( + "get", + urls.adminSearchParams, + expect.anything(), + ) }) test("admin users can set staleness and score cutoff sliders", async () => { @@ -305,11 +318,16 @@ describe("SearchPage", () => { setMockResponse.get(urls.userMe.get(), { is_learning_path_editor: true, }) - APP_SETTINGS.DEFAULT_SEARCH_MODE = "phrase" - APP_SETTINGS.DEFAULT_SEARCH_SLOP = 6 - APP_SETTINGS.DEFAULT_SEARCH_STALENESS_PENALTY = 2.5 - APP_SETTINGS.DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF = 0 - APP_SETTINGS.DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY = 90 + + setMockResponse.get(urls.adminSearchParams.get(), { + search_mode: "phrase", + slop: 6, + yearly_decay_percent: 2.5, + min_score: 0, + max_incompleteness_penalty: 90, + content_file_score_weight: 1, + }) + renderWithProviders() await waitFor(() => { const adminFacetContainer = screen.getByText("Admin Options") @@ -325,11 +343,6 @@ describe("SearchPage", () => { }) test("admin users can set the search mode and slop", async () => { - APP_SETTINGS.DEFAULT_SEARCH_MODE = "phrase" - APP_SETTINGS.DEFAULT_SEARCH_SLOP = 6 - APP_SETTINGS.DEFAULT_SEARCH_STALENESS_PENALTY = 2.5 - APP_SETTINGS.DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF = 0 - APP_SETTINGS.DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY = 90 setMockApiResponses({ search: { count: 700, @@ -352,6 +365,15 @@ test("admin users can set the search mode and slop", async () => { setMockResponse.get(urls.userMe.get(), { is_learning_path_editor: true, }) + setMockResponse.get(urls.adminSearchParams.get(), { + search_mode: "phrase", + slop: 6, + yearly_decay_percent: 2.5, + min_score: 0, + max_incompleteness_penalty: 90, + content_file_score_weight: 1, + }) + const { location } = renderWithProviders() await waitFor(() => { const adminFacetContainer = screen.getByText("Admin Options") diff --git a/frontends/mit-learn/webpack.config.js b/frontends/mit-learn/webpack.config.js index 5adf71258b..7e49fd3bbb 100644 --- a/frontends/mit-learn/webpack.config.js +++ b/frontends/mit-learn/webpack.config.js @@ -45,11 +45,6 @@ const { CSRF_COOKIE_NAME, APPZI_URL, MITOL_NOINDEX, - DEFAULT_SEARCH_MODE, - DEFAULT_SEARCH_SLOP, - DEFAULT_SEARCH_STALENESS_PENALTY, - DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF, - DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY, } = cleanEnv(process.env, { NODE_ENV: str({ choices: ["development", "production", "test"], @@ -129,26 +124,6 @@ const { desc: "Whether to include a noindex meta tag", default: true, }), - DEFAULT_SEARCH_SLOP: num({ - desc: "The default search slop", - default: 6, - }), - DEFAULT_SEARCH_STALENESS_PENALTY: num({ - desc: "The default search staleness penalty", - default: 2.5, - }), - DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF: num({ - desc: "The default search minimum score cutoff", - default: 5, - }), - DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY: num({ - desc: "The default search max incompleteness penalty", - default: 90, - }), - DEFAULT_SEARCH_MODE: str({ - desc: "The default search mode", - default: "phrase", - }), }) const MITOL_FEATURES_PREFIX = "FEATURE_" @@ -290,11 +265,6 @@ module.exports = (env, argv) => { MITOL_SUPPORT_EMAIL: JSON.stringify(MITOL_SUPPORT_EMAIL), PUBLIC_URL: JSON.stringify(PUBLIC_URL), CSRF_COOKIE_NAME: JSON.stringify(CSRF_COOKIE_NAME), - DEFAULT_SEARCH_MODE: JSON.stringify(DEFAULT_SEARCH_MODE), - DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY, - DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF, - DEFAULT_SEARCH_SLOP, - DEFAULT_SEARCH_STALENESS_PENALTY, }, }), ] diff --git a/frontends/ol-utilities/src/types/settings.d.ts b/frontends/ol-utilities/src/types/settings.d.ts index 553c75e0c8..da51840dd7 100644 --- a/frontends/ol-utilities/src/types/settings.d.ts +++ b/frontends/ol-utilities/src/types/settings.d.ts @@ -22,10 +22,5 @@ export declare global { SITE_NAME: string MITOL_SUPPORT_EMAIL: string PUBLIC_URL: string - DEFAULT_SEARCH_MODE: string - DEFAULT_SEARCH_SLOP: number - DEFAULT_SEARCH_STALENESS_PENALTY: number - DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF: number - DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY: number } } diff --git a/learning_resources_search/urls.py b/learning_resources_search/urls.py index 029ae81492..3e5825b619 100644 --- a/learning_resources_search/urls.py +++ b/learning_resources_search/urls.py @@ -5,6 +5,7 @@ from learning_resources_search.views import ( ContentFileSearchView, + LearningResourceSearchDefaultsView, LearningResourcesSearchView, UserSearchSubscriptionViewSet, ) @@ -30,7 +31,16 @@ ), ] +v0_urls = [ + path( + r"learning_resources_search_admin_params/", + LearningResourceSearchDefaultsView.as_view(), + name="learning_resources_search_admin_params", + ), +] + app_name = "lr_search" urlpatterns = [ re_path(r"^api/v1/", include((v1_urls, "v1"))), + re_path(r"^api/v0/", include((v0_urls, "v0"))), ] diff --git a/learning_resources_search/views.py b/learning_resources_search/views.py index b61a2310a9..70ad54ecba 100644 --- a/learning_resources_search/views.py +++ b/learning_resources_search/views.py @@ -4,6 +4,7 @@ from itertools import chain from django.conf import settings +from django.http import JsonResponse from django.utils.decorators import method_decorator from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view from opensearchpy.exceptions import TransportError @@ -265,3 +266,29 @@ def get(self, request): errors[key] = list(set(chain(*errors_obj.values()))) return Response(errors, status=400) + + +@action(methods=["GET"], detail=False, name="Search Defaults") +@extend_schema_view( + get=extend_schema( + parameters=None, + ), +) +class LearningResourceSearchDefaultsView(APIView): + """Learning resource search default admin param values""" + + authentication_classes = [] + permission_classes = [] + serializer_class = None + + def get(self, _): + return JsonResponse( + { + "search_mode": settings.DEFAULT_SEARCH_MODE, + "slop": settings.DEFAULT_SEARCH_SLOP, + "yearly_decay_percent": settings.DEFAULT_SEARCH_STALENESS_PENALTY, + "min_score": settings.DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF, + "max_incompleteness_penalty": settings.DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY, # noqa: E501 + "content_file_score_weight": settings.DEFAULT_SEARCH_CONTENT_FILE_SCORE_WEIGHT, # noqa: E501 + } + ) diff --git a/main/settings.py b/main/settings.py index 76d934a2cb..9e004f52f7 100644 --- a/main/settings.py +++ b/main/settings.py @@ -779,3 +779,6 @@ def get_all_config_keys(): DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY = get_float( name="DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY", default=90 ) +DEFAULT_SEARCH_CONTENT_FILE_SCORE_WEIGHT = get_float( + name="DEFAULT_SEARCH_CONTENT_FILE_SCORE_WEIGHT", default=1 +) diff --git a/openapi/specs/v0.yaml b/openapi/specs/v0.yaml index bea72ed810..b080e4cebf 100644 --- a/openapi/specs/v0.yaml +++ b/openapi/specs/v0.yaml @@ -288,6 +288,15 @@ paths: schema: $ref: '#/components/schemas/CKEditorSettings' description: '' + /api/v0/learning_resources_search_admin_params/: + get: + operationId: learning_resources_search_admin_params_retrieve + description: Learning resource search default admin param values + tags: + - learning_resources_search_admin_params + responses: + '200': + description: No response body /api/v0/news_events/: get: operationId: news_events_list